mirror of
https://github.com/jkdansereau/goldeneye-pc-port.git
synced 2026-10-23 15:52:42 +02:00
docs: subagent workflow + port-learnings cheat-sheet
Distilled from Chris Judnich's 'Decompiling a Nintendo 64 Game in 84 Days' (saved under docs/reference/): explicit per-task budgets exposed to the agent, a shared append-only learnings file, file-partitioned parallelism, cheap pre-flight passes, and a standard investigation-subagent brief template. PORT-LEARNINGS.md is a one-screen index of the recurring N64->PC bug classes into PCPortResearch.md §F; AGENTS.md points to both.
This commit is contained in:
1 parent
3a02ded079
commit
64698f6b23
5 files changed
+719
No files matched your search
@@ -136,3 +136,6 @@ ge007.crash.log
|
||||
|
||||
# Claude Code local state / agent worktrees
|
||||
.claude/
|
||||
|
||||
# saved reference webpage assets (keep the .htm, not the sidecar cruft)
|
||||
docs/reference/*_files/
|
||||
@@ -6,6 +6,7 @@
|
||||
- Active work: **PC port** modelled on the Perfect Dark PC port (same Rare "Indy" engine family).
|
||||
- **Read `docs/PCPortResearch.md` first** — architecture, GE-specific RSP deltas, phased plan, and review findings (A1–D33 in §11; current handoff + plan in §H).
|
||||
- **Starting a fresh session?** Read `docs/HANDOFF.md` — the paste-ready brief with the immediate task, standing procedure, and environment gotchas.
|
||||
- **Dispatching subagents?** `docs/AGENT-WORKFLOW.md` — task budgets/deadlines, file partitioning, pre-flight, the standard brief template. Every investigation subagent reads `docs/PORT-LEARNINGS.md` (recurring bug classes) first and appends to it.
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Agent / subagent workflow for this port
|
||||
|
||||
Guidance distilled from Chris Lewis's *"Decompiling a Nintendo 64 Game
|
||||
in 84 Days"* (`docs/reference/`, Snowboard Kids, 2026) — an AI-assisted
|
||||
N64 decomp run 7× faster than its predecessor — adapted to this project,
|
||||
which is a **PC port on top of a decomp**, not a byte-match decomp. The
|
||||
mechanics differ (we have no permuter / m2c loop; our inner loop is
|
||||
root-cause → narrow `#ifdef PORT` fix → visual verify) but the
|
||||
project-structure lessons transfer directly.
|
||||
|
||||
## 1. Every subagent task gets an explicit budget, stated in the brief
|
||||
|
||||
Their key finding: an open-ended tool (the permuter) that "runs until
|
||||
100% match or manually stopped" wrecks throughput; giving the agent a
|
||||
**deadline it can see** let it trade tool-time against other
|
||||
problem-solving and judge when to give up on a hard function.
|
||||
|
||||
Our open-ended tools are the same shape: rebuild-and-run-the-game cycles,
|
||||
gdb, env-gated probe iteration, staring at PPM dumps. So every subagent
|
||||
brief must state:
|
||||
|
||||
- a **wall/iteration budget** — e.g. "≤ N build→run→inspect cycles" or
|
||||
"~M minutes of investigation"; and
|
||||
- the **fallback on expiry**: *stop, revert probes, write up what you
|
||||
have with a confidence rating* — a good write-up of a half-solved bug
|
||||
is a deliverable, not a failure.
|
||||
|
||||
Escalate the budget as the project hardens: early bugs are cheap
|
||||
(one-line truncations, off-by-ones), so cap tight; once those are gone
|
||||
the survivors are structural (matrix handedness, format specs) and
|
||||
deserve more room. Revisit the default each session.
|
||||
|
||||
## 2. A shared, append-only learnings file that briefs point every agent at
|
||||
|
||||
They had agents record generalisable IDO quirks in
|
||||
`DECOMPILATION_LEARNINGS.md`; later agents read it and got better. "Agents
|
||||
helped document IDO, and the documentation made subsequent agents better."
|
||||
|
||||
Our equivalent already exists but is diffuse: `docs/PCPortResearch.md` §F
|
||||
is 200 KB of narrative. The **recurring PC-port quirk classes** should be
|
||||
distilled into a short cheat-sheet every subagent brief links and every
|
||||
subagent is told to append to:
|
||||
|
||||
- pointer-width struct growth (32→64) in ROM-serialized / pun-allocated
|
||||
structs — the dominant bug class (D53.2, D56, D79, D98–D102)
|
||||
- 16-byte PC `Gfx`/`Vtx` vs 8-byte N64 — reservation & copy sizing
|
||||
(D50.6, D58, D85, D95)
|
||||
- big-endian rodata read on LE: `f32` word-pairs (D73, D112), header
|
||||
offset tables (D68, D87), packed bitfields (D78, D83)
|
||||
- N64 hardware idioms fast3d doesn't emulate (fill-rect Z clear → D105;
|
||||
LOD/detail tiles → D107; K0 segment address folds → D58, D84)
|
||||
- offline sidecar converter preferred over runtime fixup for whole
|
||||
formats (D43, D69, D88)
|
||||
|
||||
This is `docs/PORT-LEARNINGS.md` — one screen, an index into §F, not a
|
||||
replacement. Every investigation-subagent brief links it; every subagent
|
||||
is told to append any new generalisable quirk before reporting.
|
||||
|
||||
## 3. Parallel work: partition by file, consolidate often
|
||||
|
||||
They ran 4 git worktrees sharded by a hash of the candidate list, and
|
||||
found the cost was **synchronisation** — a match in one worktree stayed
|
||||
invisible to the others until merged, and merging all four took >1 h.
|
||||
Their fix: make the similarity search read *all* worktrees so a new
|
||||
reference propagates immediately, and merge to main periodically anyway.
|
||||
|
||||
For us (parallel investigation subagents, not worktrees yet):
|
||||
|
||||
- **Partition tracks by the files they will touch** so patches don't
|
||||
collide. This session: Track 1 = `bg.c`, Track 2 = `chr.c`/`model*.c`,
|
||||
Track 3 = `gunfire.c` + a new audit doc — disjoint, merged clean.
|
||||
A brief that can't be file-partitioned shouldn't be parallelised.
|
||||
- Subagents must **revert their own probes** before reporting (leave only
|
||||
committed, env-gated, capped probes) and must **not** touch another
|
||||
track's uncommitted edits — call them out in the report instead.
|
||||
- The integrator (main session) consolidates to `master` per-track with
|
||||
the §F write-up, then the next round of agents starts from a clean
|
||||
tree. Don't let investigation branches drift.
|
||||
- If we move to real worktrees: `data/` must be **copied, never
|
||||
junctioned** (see the `data-dir-junction-hazard` memory —
|
||||
`git worktree remove --force` follows the junction and deletes the real
|
||||
baserom + sidecars).
|
||||
|
||||
## 4. Cheap automated pass before spending agent tokens
|
||||
|
||||
They scripted `m2c` across every unmatched function first (0.93 % hit
|
||||
rate, but near-free) and told agents to exhaust known SDK
|
||||
versions / compiler flags / `#if` paths before writing original code.
|
||||
|
||||
Our pre-flight before dispatching an investigation agent:
|
||||
|
||||
- `./build-pc.sh` green + `/linkcheck` clean
|
||||
- run the existing env-gated probes for the area (`GE_D96`, `GE_D104`,
|
||||
`GE_D69*`, …) and attach the output to the brief
|
||||
- `GE_PCDUMP` + `tools_pc/pixcount.py` baseline capture
|
||||
- grep `docs/PCPortResearch.md` §F for a prior instance of the bug class
|
||||
- check `PD_PORT_CHECKOUT` for the PD analogue
|
||||
(already a non-negotiable) — the brief should name the file to look at
|
||||
|
||||
Give the agent the results, not just the task. "Give the agent the
|
||||
findings, not just the problem" is the through-line of the whole post.
|
||||
|
||||
## 5. What does *not* transfer
|
||||
|
||||
- **Model choice.** Their (unscientific) take was Codex > Claude for
|
||||
reproducing IDO codegen. Our task is behavioural correctness, not
|
||||
compiler-output matching — not comparable; ignore.
|
||||
- **The permuter / match-percentage loop.** We don't bit-match. Our
|
||||
"done" bar is: no fault + visually correct vs. N64 footage
|
||||
(`docs/reference/n64-footage-*`), measured with `pixcount.py`.
|
||||
- **"Understanding the function is the easy part."** True for decomp,
|
||||
inverted here — our hard part is often *what N64 hardware/ABI behaviour
|
||||
the C is silently relying on*.
|
||||
|
||||
## Standard investigation-subagent brief template
|
||||
|
||||
```
|
||||
Repo: REPO_ROOT (git, master, Windows; PowerShell + Bash).
|
||||
READ FIRST: docs/HANDOFF.md (top), docs/PORT-LEARNINGS.md,
|
||||
docs/PCPortResearch.md §F <specific Dxx entries>, AGENTS.md non-negotiables.
|
||||
TASK: <one bug, one subsystem>.
|
||||
FILES YOU MAY TOUCH: <disjoint from other running agents>.
|
||||
KNOWN-GOOD / RULED OUT: <list — do not re-investigate>.
|
||||
PRE-FLIGHT ATTACHED: <probe output, pixcount baseline, pd_port pointer>.
|
||||
BUDGET: <N build→run cycles / ~M min>. On expiry: revert probes, write up with confidence.
|
||||
CONSTRAINTS: no game-logic changes; ABI/layout/format only, #ifdef PORT, documented in §F.
|
||||
revert your own temp probes; do not touch other agents' uncommitted edits.
|
||||
REPORT: (a) root cause + file:line evidence (b) fix diff or why-not
|
||||
(c) probes left in tree (d) confidence. Append any generalisable
|
||||
quirk to docs/PORT-LEARNINGS.md.
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
# PORT-LEARNINGS — recurring N64→PC bug classes
|
||||
|
||||
One-screen index into `docs/PCPortResearch.md` §F. **Every investigation
|
||||
subagent reads this first and appends any new generalisable quirk.**
|
||||
Details, evidence, and the fix for each instance live in §F under the
|
||||
cited Dxx label.
|
||||
|
||||
## A. Pointer-width struct growth (32→64) — the dominant class
|
||||
|
||||
A decomp struct with pointer fields, or one pun-allocated into a fixed
|
||||
N64-sized hole / hardcoded byte count, is **larger on x86-64**. Reading it
|
||||
from ROM bytes misaligns; allocating it N64-sized overruns adjacent
|
||||
state.
|
||||
|
||||
- Symptom: garbage pointer deref, corrupted neighbour field, delayed
|
||||
fault far from the cause.
|
||||
- Fix: store embedded ROM addresses as `u32`, cast at use site (PD ground
|
||||
truth); or give real inline storage / `sizeof()`-based alloc under
|
||||
`#ifdef PORT`.
|
||||
- Instances: D53.2 (ModelSlot pun), D56 (watch Model raw offsets), D57
|
||||
(rwdata record count), D67 (image_entry), D79 (bg_room_data), D98
|
||||
(struct player alloc), D100 (player.model inline Model), D101/D102
|
||||
(ModelNode*/weapon Model puns), D115 (gunfire THROW* raw offsets).
|
||||
- **Open landmine:** raw hardcoded-offset accessors into `struct player`
|
||||
/ `struct hand` — see `docs/AUDIT-M6-player-offsets.md`.
|
||||
|
||||
## B. 16-byte PC `Gfx` / `Vtx` vs 8-byte N64
|
||||
|
||||
Any buffer reservation, `memcpy` size, slot stride, or pool budget
|
||||
expressed in N64 `Gfx`/`Vtx` units is **half-size** on PC.
|
||||
|
||||
- Instances: D50.6 (texCopyGdls copied only w0 of each 16-byte slot),
|
||||
D58 (DL reserve 0x100→0x200), D85 (`bgWidenRoomGdl` 8→16 + bswap),
|
||||
D95 (2× master-DL buffer + raised mempool ceiling).
|
||||
|
||||
## C. Big-endian rodata / ROM data read on little-endian PC
|
||||
|
||||
ROM assets and compiled-in `.rodata` are big-endian. Anything not run
|
||||
through a converter or a runtime bswap fixup reads scrambled.
|
||||
|
||||
- `f32` values are BE **word pairs** — a naive byte reversal off-by-one
|
||||
corrupts every float: D73 (sinf/cosf `du` pairs → `DVAL()` macro),
|
||||
D112 (`d43_emit.py put_f32` `src[doff:doff+4][::-1]`).
|
||||
- Header offset tables / pointers: D54 (cseq ALMidiHdr), D68
|
||||
(Globalimagetable), D87 (ramromfilestructure), D88 (Usetup* tables).
|
||||
- Packed bitfields cross byte boundaries differently: D78 / D83
|
||||
(StandTile id/room, header mid/tail).
|
||||
- **Rule:** prefer an offline sidecar converter (D43, D69, D88 pattern,
|
||||
`tools_pc/d*_emit.py`) over a runtime fixup for a whole format.
|
||||
|
||||
## D. N64 hardware idioms fast3d does not emulate
|
||||
|
||||
- Z buffer cleared by pointing the colour image at it + fill-rect → does
|
||||
nothing in fast3d; must emit `G_CLEAR_DEPTH_EXT` (D105).
|
||||
- LOD / detail mip tiles: fast3d fabricates a crop when detail textures
|
||||
are off → force base tile (D107).
|
||||
- K0 segment-address folds (`OS_K0_TO_PHYSICAL | 0x80000000`) and
|
||||
hand-inlined `BG_SEG_TO_PTR` need the `(u32)` 32-bit wrap the macro has
|
||||
(D58, D84).
|
||||
- Portal near-plane: z==0 clip points project to ±1e20; x86-64 float
|
||||
garbage can come back `min>max` / non-finite and slip past a
|
||||
degenerate-box check that clamps to full-screen on N64 (D106).
|
||||
|
||||
## E. Process / method notes
|
||||
|
||||
- Investigation loop is: reproduce → env-gated capped probe → root-cause
|
||||
→ narrow `#ifdef PORT` fix → visual verify (`GE_PCDUMP` +
|
||||
`tools_pc/pixcount.py` vs `docs/reference/n64-footage-*`).
|
||||
- gdb **launch** mode is too slow for timing-dependent faults; gdb
|
||||
**attach** to an already-running process is fine and fast.
|
||||
- Check `PD_PORT_CHECKOUT` for the PD analogue before
|
||||
writing anything new — same Rare engine family.
|
||||
- Don't re-investigate a closed §F finding or re-derive a format spec
|
||||
that already has a converter.
|
||||
@@ -0,0 +1,510 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-au" data-darkreader-mode="dynamic" data-darkreader-scheme="dark"><head>
|
||||
<meta http-equiv="content-type" content="text/html; charset=UTF-8"><style class="darkreader darkreader--fallback" media="screen"></style><style class="darkreader darkreader--user-agent" media="screen">@layer {
|
||||
html {
|
||||
background-color: var(--darkreader-background-ffffff, #181a1b) !important;
|
||||
}
|
||||
html {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
iframe {
|
||||
color-scheme: dark !important;
|
||||
}
|
||||
html, body {
|
||||
background-color: var(--darkreader-background-ffffff, #181a1b);
|
||||
}
|
||||
html, body {
|
||||
border-color: var(--darkreader-border-4c4c4c, #736b5e);
|
||||
color: var(--darkreader-text-000000, #e8e6e3);
|
||||
}
|
||||
a {
|
||||
color: var(--darkreader-text-0040ff, #3391ff);
|
||||
}
|
||||
table {
|
||||
border-color: var(--darkreader-border-808080, #545b5e);
|
||||
}
|
||||
mark {
|
||||
color: var(--darkreader-text-000000, #e8e6e3);
|
||||
}
|
||||
::placeholder {
|
||||
color: var(--darkreader-text-a9a9a9, #b2aba1);
|
||||
}
|
||||
input:-webkit-autofill,
|
||||
textarea:-webkit-autofill,
|
||||
select:-webkit-autofill {
|
||||
background-color: var(--darkreader-background-faffbd, #404400) !important;
|
||||
color: var(--darkreader-text-000000, #e8e6e3) !important;
|
||||
}
|
||||
* {
|
||||
scrollbar-color: var(--darkreader-background-b0b0b0, #454a4d) var(--darkreader-background-f1f1f1, #202324);
|
||||
}
|
||||
::selection {
|
||||
background-color: var(--darkreader-background-0060d4, #004daa) !important;
|
||||
color: var(--darkreader-text-ffffff, #e8e6e3) !important;
|
||||
}
|
||||
::-moz-selection {
|
||||
background-color: var(--darkreader-background-0060d4, #004daa) !important;
|
||||
color: var(--darkreader-text-ffffff, #e8e6e3) !important;
|
||||
}
|
||||
}</style><style class="darkreader darkreader--text" media="screen"></style><style class="darkreader darkreader--invert" media="screen">.jfk-bubble.gtx-bubble, .captcheck_answer_label > input + img, span#closed_text > img[src^="https://www.gstatic.com/images/branding/googlelogo"], span[data-href^="https://www.hcaptcha.com/"] > #icon, img.Wirisformula, a[data-testid="headerMediumLogo"]>svg, .d2l-navigation-link-image-container, .d2l-iframe-loading-container, div.mc-ip-hide[style*="background: rgb(255, 255, 255) !important"], [style*="color: rgb(0, 0, 1) !important;"] {
|
||||
filter: invert(100%) hue-rotate(180deg) contrast(90%) !important;
|
||||
}</style><style class="darkreader darkreader--inline" media="screen">[data-darkreader-inline-bgcolor] {
|
||||
background-color: var(--darkreader-inline-bgcolor) !important;
|
||||
}
|
||||
[data-darkreader-inline-bgimage] {
|
||||
background-image: var(--darkreader-inline-bgimage) !important;
|
||||
}
|
||||
[data-darkreader-inline-border] {
|
||||
border-color: var(--darkreader-inline-border) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-bottom] {
|
||||
border-bottom-color: var(--darkreader-inline-border-bottom) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-left] {
|
||||
border-left-color: var(--darkreader-inline-border-left) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-right] {
|
||||
border-right-color: var(--darkreader-inline-border-right) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-top] {
|
||||
border-top-color: var(--darkreader-inline-border-top) !important;
|
||||
}
|
||||
[data-darkreader-inline-boxshadow] {
|
||||
box-shadow: var(--darkreader-inline-boxshadow) !important;
|
||||
}
|
||||
[data-darkreader-inline-color] {
|
||||
color: var(--darkreader-inline-color) !important;
|
||||
}
|
||||
[data-darkreader-inline-fill] {
|
||||
fill: var(--darkreader-inline-fill) !important;
|
||||
}
|
||||
[data-darkreader-inline-stroke] {
|
||||
stroke: var(--darkreader-inline-stroke) !important;
|
||||
}
|
||||
[data-darkreader-inline-outline] {
|
||||
outline-color: var(--darkreader-inline-outline) !important;
|
||||
}
|
||||
[data-darkreader-inline-stopcolor] {
|
||||
stop-color: var(--darkreader-inline-stopcolor) !important;
|
||||
}
|
||||
[data-darkreader-inline-bg] {
|
||||
background: var(--darkreader-inline-bg) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-short] {
|
||||
border: var(--darkreader-inline-border-short) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-bottom-short] {
|
||||
border-bottom: var(--darkreader-inline-border-bottom-short) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-left-short] {
|
||||
border-left: var(--darkreader-inline-border-left-short) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-right-short] {
|
||||
border-right: var(--darkreader-inline-border-right-short) !important;
|
||||
}
|
||||
[data-darkreader-inline-border-top-short] {
|
||||
border-top: var(--darkreader-inline-border-top-short) !important;
|
||||
}
|
||||
[data-darkreader-inline-invert] {
|
||||
filter: invert(100%) hue-rotate(180deg);
|
||||
}</style><style class="darkreader darkreader--variables" media="screen">:root {
|
||||
--darkreader-neutral-background: var(--darkreader-background-ffffff, #181a1b);
|
||||
--darkreader-neutral-text: var(--darkreader-text-000000, #e8e6e3);
|
||||
--darkreader-selection-background: var(--darkreader-background-0060d4, #004daa);
|
||||
--darkreader-selection-text: var(--darkreader-text-ffffff, #e8e6e3);
|
||||
}</style><style class="darkreader darkreader--root-vars" media="screen"></style><meta http-equiv="X-Clacks-Overhead" content="GNU Terry Pratchett"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Decompiling a Nintendo 64 Game in 84 Days | Chris' Blog</title><meta name="title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta name="description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta name="keywords" content=""><meta property="og:url" content="http://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/"><meta property="og:site_name" content="Chris' Blog"><meta property="og:title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta property="og:description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta property="og:locale" content="en_au"><meta property="og:type" content="article"><meta property="article:section" content="blog"><meta property="article:published_time" content="2026-08-26T20:23:59-07:00"><meta property="article:modified_time" content="2026-08-26T20:23:59-07:00"><meta property="og:image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="twitter:title" content="Decompiling a Nintendo 64 Game in 84 Days"><meta name="twitter:description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta itemprop="name" content="Decompiling a Nintendo 64 Game in 84 Days"><meta itemprop="description" content="The original Snowboard Kids has been fully decompiled in only 84 days! In this post I delve into what changed and what lessons might apply to future projects."><meta itemprop="datePublished" content="2026-08-26T20:23:59-07:00"><meta itemprop="dateModified" content="2026-08-26T20:23:59-07:00"><meta itemprop="wordCount" content="2266"><meta itemprop="image" content="http://blog.chrislewis.au/snowboard-kids-progress.png"><meta name="referrer" content="no-referrer-when-downgrade"><style>:root{--width:720px;--font-main:Verdana, sans-serif;--font-secondary:Verdana, sans-serif;--font-scale:1em;--background-color:#fff;--heading-color:#222;--text-color:#444;--link-color:#3273dc;--visited-color:#8b6fcb;--blockquote-color:#222}@media(prefers-color-scheme:dark){:root{--background-color:#01242e;--heading-color:#eee;--text-color:#ddd;--link-color:#8cc2dd;--visited-color:#8b6fcb;--blockquote-color:#ccc}}body{font-family:var(--font-secondary);font-size:var(--font-scale);margin:auto;padding:20px;max-width:var(--width);text-align:left;background-color:var(--background-color);word-wrap:break-word;overflow-wrap:break-word;line-height:1.5;color:var(--text-color)}h1,h2,h3,h4,h5,h6{font-family:var(--font-main);color:var(--heading-color)}a{color:var(--link-color);cursor:pointer;text-decoration:none}a:hover{text-decoration:underline}nav a{margin-right:8px}strong,b{color:var(--heading-color)}button{margin:0;cursor:pointer}time{font-family:monospace;font-style:normal;font-size:15px}main{line-height:1.6}table{width:100%}hr{border:0;border-top:1px dashed}img{max-width:100%}code{font-family:monospace;padding:2px;border-radius:3px}blockquote{border-left:1px solid #999;color:var(--blockquote-color);padding-left:20px;font-style:italic}footer{padding:25px 0;text-align:center}.title:hover{text-decoration:none}.title h1{font-size:1.5em}.inline{width:auto!important}.highlight,.code{border-radius:3px;margin-block-start:1em;margin-block-end:1em;overflow-x:auto}ul.blog-posts{list-style-type:none;padding:unset}ul.blog-posts li{display:flex}ul.blog-posts li span{flex:0 0 130px}ul.blog-posts li a:visited{color:var(--visited-color)}</style><style class="darkreader darkreader--sync" media="screen"></style><style type="text/css">img{margin:0 auto;display:block}@media(prefers-color-scheme:dark){img[src$="#darksafe"]{filter:invert(1)}}.image-caption{font-size:calc(var(--font-scale) * .8);margin:0 20px;font-style:italic;opacity:.9;text-align:center}.highlight{background-color:#272822}.bluesky-comments{margin-top:2.5rem}.bluesky-comments h2{font-size:1.25em;margin-bottom:.5rem}.bluesky-comments__status,.bluesky-comments__summary{font-size:calc(var(--font-scale) * .9)}.bluesky-comments__summary{align-items:center;display:flex;flex-wrap:wrap;gap:.5rem 1rem}.bluesky-comments__reply{display:grid;gap:.75rem;grid-template-columns:44px 1fr;margin-top:1rem}.bluesky-comments__avatar{aspect-ratio:1;border-radius:50%;display:block;height:44px;object-fit:cover;width:44px}.bluesky-comments__avatar--blank{background:var(--blockquote-color);opacity:.25}.bLine truncated
|
||||
background-color: white !important;
|
||||
}
|
||||
.vimvixen-hint {
|
||||
background-color: var(--darkreader-background-ffd76e, #684b00) !important;
|
||||
border-color: var(--darkreader-background-c59d00, #9e7e00) !important;
|
||||
color: var(--darkreader-text-302505, #d7d4cf) !important;
|
||||
}
|
||||
#vimvixen-console-frame {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
::placeholder {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
#edge-translate-panel-body,
|
||||
.MuiTypography-body1,
|
||||
.nfe-quote-text {
|
||||
color: var(--darkreader-neutral-text) !important;
|
||||
}
|
||||
gr-main-header {
|
||||
background-color: var(--darkreader-background-add8e6, #1b4958) !important;
|
||||
}
|
||||
.tou-z65h9k,
|
||||
.tou-mignzq,
|
||||
.tou-1b6i2ox,
|
||||
.tou-lnqlqk {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
.tou-75mvi {
|
||||
background-color: var(--darkreader-background-cfecf5, #0f3a47) !important;
|
||||
}
|
||||
.tou-ta9e87,
|
||||
.tou-1w3fhi0,
|
||||
.tou-1b8t2us,
|
||||
.tou-py7lfi,
|
||||
.tou-1lpmd9d,
|
||||
.tou-1frrtv8,
|
||||
.tou-17ezmgn {
|
||||
background-color: var(--darkreader-background-f5f5f5, #1e2021) !important;
|
||||
}
|
||||
.tou-uknfeu {
|
||||
background-color: var(--darkreader-background-faedda, #432c09) !important;
|
||||
}
|
||||
.tou-6i3zyv {
|
||||
background-color: var(--darkreader-background-85c3d8, #245d70) !important;
|
||||
}
|
||||
div.mermaid-viewer-control-panel .btn {
|
||||
background-color: var(--darkreader-neutral-background);
|
||||
fill: var(--darkreader-neutral-text);
|
||||
}
|
||||
svg g rect.er {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
svg g rect.er.entityBox {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
svg g rect.er.attributeBoxOdd {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
svg g rect.er.attributeBoxEven {
|
||||
fill: var(--darkreader-selection-background);
|
||||
fill-opacity: 0.8 !important;
|
||||
}
|
||||
svg rect.er.relationshipLabelBox {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
svg g g.nodes rect,
|
||||
svg g g.nodes polygon {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
svg g rect.task {
|
||||
fill: var(--darkreader-selection-background) !important;
|
||||
}
|
||||
svg line.messageLine0,
|
||||
svg line.messageLine1 {
|
||||
stroke: var(--darkreader-neutral-text) !important;
|
||||
}
|
||||
div.mermaid .actor {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
mitid-authenticators-code-app > .code-app-container {
|
||||
background-color: white !important;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
iframe#unpaywall[src$="unpaywall.html"] {
|
||||
color-scheme: light !important;
|
||||
}
|
||||
select {
|
||||
--darkreader-bg--form-control-background-color: rgba(22, 22, 22, 0) !important;
|
||||
}
|
||||
body#tumblr {
|
||||
--darkreader-bg--secondary-accent: 31, 32, 34 !important;
|
||||
--darkreader-bg--white: 23, 23, 23 !important;
|
||||
--darkreader-text--black: 228, 224, 218 !important;
|
||||
}
|
||||
:host {
|
||||
--d2l-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
--d2l-button-icon-background-color-hover: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
--d2l-color-ferrite: var(--darkreader-neutral-text) !important;
|
||||
--d2l-color-sylvite: var(--darkreader-bg--d2l-color-sylvite) !important;
|
||||
--d2l-dropdown-background-color: var(--darkreader-neutral-background) !important;
|
||||
--d2l-dropdown-border-color: var(--darkreader-border--d2l-color-mica) !important;
|
||||
--d2l-input-backgroud-color: var(--darkreader-neutral-background) !important;
|
||||
--d2l-menu-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
--d2l-tooltip-background-color: var(--darkreader-neutral-background) !important;
|
||||
--d2l-tooltip-border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
}
|
||||
:host([_floating]) .d2l-floating-buttons-container {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
border-top-color: var(--darkreader-border--d2l-color-mica) !important;
|
||||
opacity: 0.88 !important;
|
||||
}
|
||||
d2l-card {
|
||||
background: var(--darkreader-neutral-background) !important;
|
||||
border-color: var(--darkreader-border--d2l-color-gypsum) !important;
|
||||
}
|
||||
d2l-dropdown-content > div,
|
||||
d2l-menu-item {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
d2l-empty-state-simple {
|
||||
border-color: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
}
|
||||
.d2l-button-filter > ul > li > a.vui-button {
|
||||
border-color: var(--darkreader-border--d2l-color-mica) !important;
|
||||
}
|
||||
.d2l-label-text:has(.d2l-button-subtle-content):hover,
|
||||
.d2l-label-text:has(.d2l-button-subtle-content):focus,
|
||||
.d2l-label-text:has(.d2l-button-subtle-content):active {
|
||||
background-color: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
}
|
||||
.d2l-navigation-centerer {
|
||||
color: inherit !important;
|
||||
}
|
||||
.d2l-tabs-layout {
|
||||
border-color: var(--darkreader-border--d2l-color-gypsum) !important;
|
||||
}
|
||||
.d2l-input,
|
||||
.d2l-calendar-date,
|
||||
.d2l-htmleditor-container {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
.d2l-collapsible-panel {
|
||||
border: 1px solid var(--darkreader-border--d2l-color-mica) !important;
|
||||
border-radius: 0.4rem !important;
|
||||
}
|
||||
.d2l-collapsible-panel-divider {
|
||||
border-bottom: 1px solid var(--darkreader-border--d2l-color-mica) !important;
|
||||
}
|
||||
.d2l-w2d-flex {
|
||||
border-bottom: 2px solid var(--darkreader-border--d2l-color-mica) !important;
|
||||
}
|
||||
.d2l-collapsible-panel scrolled,
|
||||
.d2l-collapsible-panel-header,
|
||||
.d2l-w2d-collection-fixed {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
.d2l-loading-spinner-bg {
|
||||
fill: var(--darkreader-bg--d2l-color-gypsum) !important;
|
||||
}
|
||||
.d2l-loading-spinner-bg-stroke {
|
||||
stroke: var(--darkreader-border--d2l-color-mica) !important;
|
||||
}
|
||||
.d2l-loading-spinner-wrapper svg path,
|
||||
.d2l-loading-spinner-wrapper svg circle {
|
||||
fill: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
.d2l-twopanelselector-side.d2l-twopanelselector-side-sep {
|
||||
background: var(--darkreader-bg--d2l-color-mica) !important;
|
||||
}
|
||||
.d2l-le-TreeAccordionItem-anchor::before {
|
||||
background: var(--darkreader-bg--d2l-color-corundum) !important;
|
||||
}
|
||||
table[class|="moz-header"] {
|
||||
background: none;
|
||||
}
|
||||
*[style*="color: rgba(0,0,0,"]:not([style*="color: rgba(0,0,0,0)"]),
|
||||
*[style*="color: rgba(0, 0, 0,"]:not([style*="color: rgba(0, 0, 0, 0)"]),
|
||||
*[style*="color:#000"],
|
||||
*[style*="color:black"],
|
||||
*[style*="color:#282828"],
|
||||
*[style*="color:#444"],
|
||||
*[style*="color: #222222"] {
|
||||
color: var(--darkreader-neutral-text) !important;
|
||||
}
|
||||
*[style*="background:#fff"],
|
||||
*[style*="background-color:#fff"],
|
||||
*[style*="background: #fff"] {
|
||||
background: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
*[bgcolor="#F5F5F5"],
|
||||
*[style*="background-color: #F5F5F5"] {
|
||||
background: var(--darkreader-background-f5f5f5, #1e2021) !important;
|
||||
}
|
||||
*[style*="background: #e6e6e6"] {
|
||||
background: var(--darkreader-background-e6e6e6, #26292b) !important;
|
||||
}
|
||||
body[style*="background: rgb(255, 255, 255) !important"] *:not(a),
|
||||
.body[style*="background-color: rgb(254, 254, 254) !important"] *:not(a) {
|
||||
background-color: var(--darkreader-neutral-background) !important;
|
||||
}
|
||||
div[style*="www.gstatic.com/android/"] {
|
||||
background: none !important;
|
||||
}
|
||||
*[style*="background:#f9f9f9"] {
|
||||
background: var(--darkreader-background-f9f9f9, #1b1e1f) !important;
|
||||
}
|
||||
body.mediawiki.skin-vector #mw-page-base {
|
||||
background-image: none !important;
|
||||
}
|
||||
.overflow-hidden .spinning-bg,
|
||||
.overflow-hidden .solid-bg {
|
||||
background: transparent !important;
|
||||
}
|
||||
embed[type="application/pdf"] { filter: invert(100%) contrast(90%); }</style><style></style></head><body><header><a href="https://blog.chrislewis.au/" class="title"><h2>Chris' Blog</h2></a><nav></nav></header><main><h1>Decompiling a Nintendo 64 Game in 84 Days</h1><p><i><time datetime="2026-08-26">26 Aug, 2026</time></i></p><content><p>I’m very pleased to announce that the original <em>Snowboard Kids</em> is now 100% decompiled! This means that all functions<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup> have matching C implementations that, when compiled, produce identical machine code to the original game.</p><p>This was obviously not a one-person effort. I am particularly grateful to <a href="https://github.com/inspectredc">inspectredc</a>, <a href="https://github.com/Bl00D4NGEL">Bl00D4NGEL</a>, and <a href="https://github.com/queueRAM">queueRAM</a> for their significant contributions to the project. No amount of AI would have been able to replace them.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> I would also like to thank <a href="https://github.com/iFuzzle">iFuzzle</a>, <a href="https://github.com/JamesBLewis">JamesBLewis</a>, and <a href="https://github.com/douglasjv">douglasjv</a> for lending their tokens to the cause.</p><p>My
|
||||
hope is that a full decompilation will prove useful to the Snowboard
|
||||
Kids community. Speedrunners in particular have long focused on the
|
||||
first game. Working source code can help shed light on externally
|
||||
observed phenomena such as CPU pathing and the exact factors
|
||||
contributing to player speed. A full understanding of the source code
|
||||
will also be useful for static recompilation and more ambitious modding
|
||||
efforts in the future.</p><p>The speed at which the project was finished is also noteworthy. <em>Snowboard Kids</em> took only 84 days to decompile compared with 596 days for <em>Snowboard Kids 2</em>, roughly one-seventh of the elapsed time.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p><p><a href="https://decomp.dev/cdlewis/snowboardkids-decomp"><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/snowboard-kids-progress_aOx2.svg" alt="chart showing weekly Snowboard Kids decompilation progress"></a></p><p class="image-caption"><a href="https://decomp.dev/cdlewis/snowboardkids-decomp">Weekly progress of the Snowboard Kids decompilation.</a></p><p></p><p>What
|
||||
explains this difference? Well, it’s 2026, so the answer is at least
|
||||
partially AI. But it would be a gross oversimplification to attribute
|
||||
the difference entirely to LLMs.</p><h2 id="what-was-different">What Was Different</h2><p>To
|
||||
state the obvious, I was not starting from scratch. By this point I had
|
||||
already spent nearly two years on a similar project and was vastly
|
||||
faster than when I began. This advantage is difficult to quantify and
|
||||
was somewhat offset by new challenges such as working with a different
|
||||
compiler.</p><p>Overall, roughly 4.8% of matching commits involved expert intervention.<sup id="fnref:4"><a href="#fn:4" class="footnote-ref" role="doc-noteref">4</a></sup>
|
||||
I have already credited these amazing people once, but it’s worth
|
||||
reiterating. This project would not have been possible without
|
||||
significant help from the decompilation community, particularly <a href="https://github.com/inspectredc">inspectredc</a>, <a href="https://github.com/queueRAM">queueRAM</a>, and <a href="https://github.com/Bl00D4NGEL">Bl00D4NGEL</a> <3.</p><p>The difficulty generally didn’t come from trying to understand what a function did<sup id="fnref:5"><a href="#fn:5" class="footnote-ref" role="doc-noteref">5</a></sup> but rather how that logic was expressed in C and how the resulting code was compiled. <em>Snowboard Kids</em> was compiled with IDO 5.3 rather than the GCC 2.7.2 compiler used by <em>Snowboard Kids 2</em>.</p><p>Most
|
||||
programmers will be familiar with GCC. It is a widely used open-source
|
||||
compiler still in active development today. IDO, on the other hand, was a
|
||||
proprietary compiler developed by SGI, whose own story is closely
|
||||
entwined with that of the Nintendo 64.<sup id="fnref:6"><a href="#fn:6" class="footnote-ref" role="doc-noteref">6</a></sup>
|
||||
Its source was not available, and its original development environment
|
||||
was tied to obsolete SGI hardware and software. To use and properly
|
||||
understand it today, the decompilation community has had to
|
||||
reverse-engineer and <a href="https://github.com/n64decomp/ido">decompile parts of the compiler toolchain</a> as well as <a href="https://github.com/decompals/ido-static-recomp">statically recompile the IDO 5.3 and 7.1 suites</a> to run on modern hardware. That closed history makes IDO harder to reason about than an open compiler such as GCC.</p><p>IDO splits optimisation and code generation across several different passes, transforming code quite aggressively along the way.<sup id="fnref:7"><a href="#fn:7" class="footnote-ref" role="doc-noteref">7</a></sup> Tiny changes to the C can then ripple through those passes and produce a completely different register allocation.</p><p>The
|
||||
community has made great strides in understanding IDO and its quirks,
|
||||
but this remains more of an art than a science. LLMs and I are not
|
||||
particularly good at reproducing its output. The usual workflow, for
|
||||
both me and the agents, was to figure out what a function did and then
|
||||
write C that approximated that purpose. From there, small tweaks
|
||||
assisted by the <a href="https://github.com/simonlindholm/decomp-permuter">permuter</a>
|
||||
could catch any remaining differences. But you can’t permute yourself
|
||||
into a match in all cases, particularly when the underlying structure is
|
||||
wrong. IDO’s behaviour made this workflow far less predictable.</p><p>A motivated human team with the right expertise and intuition can match or even exceed the pace of the <em>Snowboard Kids</em> decompilation. The <a href="https://github.com/gcsmith/Pilotwings64Decomp"><em>Pilotwings 64</em></a> decompilation was completed in only 74 days.<sup id="fnref:8"><a href="#fn:8" class="footnote-ref" role="doc-noteref">8</a></sup> <em>Pilotwings 64</em> had 16% fewer functions than <em>Snowboard Kids</em>, but more compiled code overall, so this is not a clean comparison either.</p><p><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/pilotwings_aOx2.webp" alt="screenshot of Pilotwings 64"></p><p class="image-caption">Pilotwings 64, a charming flight simulator, was a Nintendo 64 launch title and remains a cult classic to this day.</p><p></p><p><em>Snowboard Kids</em> was also smaller than its sequel, containing 2,145 functions compared with 2,995 in <em>Snowboard Kids 2</em>. Function count is a crude measure of difficulty, but there were simply fewer functions to decompile.</p><h2 id="where-agents-did-help">Where Agents Did Help</h2><p>I’ve <a href="https://blog.chrislewis.au/the-long-tail-of-llm-assisted-decompilation/">already written elsewhere</a>
|
||||
about using agents to decompile functions. The same basic process was
|
||||
used here, so I’ll focus on what changed. Unlike the previous project,
|
||||
this one began with access to frontier models and a capable <a href="https://github.com/cdlewis/nigel">agent harness</a>.</p><h3 id="library-code-and-other-low-hanging-fruit">Library Code and Other Low-Hanging Fruit</h3><p>I
|
||||
was interested to see how agents would fare during the early stages of a
|
||||
project. One area where they thrived was matching standard-library
|
||||
code. In theory, this is the most obvious chunk of almost any Nintendo
|
||||
64 decompilation. The code is not unique to the game, and versions of it
|
||||
are available online. <em>Snowboard Kids</em> contains more than a hundred source segments from Nintendo’s <code>libultra</code>, alongside functions from the <code>libmus</code> audio library. Tools such as <a href="https://github.com/shygoo/n64sym">N64Sym</a> can identify probable library functions in the ROM.</p><p>This
|
||||
pass was fairly successful. The main stumbling block was convincing
|
||||
agents to rely on the existing library source rather than decompile the
|
||||
same functions again from scratch. This required stronger prompting.
|
||||
Once a likely library function was identified, agents were instructed to
|
||||
treat the corresponding source as their starting point and exhaust
|
||||
plausible SDK versions, compiler options, and conditional compilation
|
||||
paths before attempting their own implementation.</p><p>Another optimisation was to have an agent write a script that ran <a href="https://github.com/matt-kempster/m2c"><code>m2c</code></a>
|
||||
against every unmatched function and automatically integrated any exact
|
||||
matches, rather than relying on agents to attempt those functions
|
||||
individually.<sup id="fnref:9"><a href="#fn:9" class="footnote-ref" role="doc-noteref">9</a></sup>
|
||||
The script matched only 17 of 1,830 functions, an incredibly low
|
||||
success rate of 0.93%, but anything matched this way was cheaper than
|
||||
burning agent tokens.</p><h3 id="ido-tooling-and-skills">IDO Tooling and Skills</h3><p>IDO
|
||||
is weird, but it is often weird in recurring ways. Successfully
|
||||
matching one function could reveal a compiler quirk that applied to many
|
||||
others. Codex has become better at carrying lessons between tasks
|
||||
through features such as local <a href="https://learn.chatgpt.com/docs/customization/memories">memories</a>. To make those lessons available beyond a single agent, I prompted agents to record observed IDO behaviour in a <a href="https://github.com/cdlewis/snowboardkids-decomp/blob/main/DECOMPILATION_LEARNINGS.md">DECOMPILATION_LEARNINGS.md</a>
|
||||
file. When an agent discovered a generalisable compiler quirk, it could
|
||||
record the evidence there for later attempts. This created a useful
|
||||
feedback loop. Agents helped document IDO, and the resulting
|
||||
documentation made subsequent agents better at matching IDO code.</p><p>But the most useful resource was <a href="https://github.com/akratch/n64-decomp-workbench">N64 Decomp Workbench</a>,
|
||||
a collection of tooling and documentation for debugging late-stage MIPS
|
||||
decompilation mismatches. It can classify mismatches, account for
|
||||
relocations, replay individual compiler passes, and help distinguish a
|
||||
structural problem from a register-allocation problem. Pass replay
|
||||
requires the relevant compiler binaries and project-specific setup, but
|
||||
once configured it exposes information that a raw assembly diff cannot. A
|
||||
raw diff tells you that two functions differ. The Workbench can give
|
||||
agents a much better idea of why the functions differ and what sort of
|
||||
change might fix them.</p><h3 id="worktrees-and-synchronisation">Worktrees and Synchronisation</h3><p>For
|
||||
this project, I ran the decompilation harness across four Git
|
||||
worktrees. Each worktree gave an agent an independent copy of the
|
||||
repository, allowing several functions to be attempted in parallel.</p><p>One small but useful improvement was to give every task an explicit deadline and expose that deadline to the agent. During the <em>Snowboard Kids 2</em>
|
||||
project, agents often struggled to use the permuter effectively because
|
||||
it would continue running until it found a 100% match or was manually
|
||||
stopped. An explicit deadline allowed agents to set sensible timeouts
|
||||
and trade off permuting time against other forms of problem-solving.
|
||||
Anecdotally, it also helped them judge how long to keep working on a
|
||||
difficult function before giving up. I could then increase the time
|
||||
allowance as the easy functions disappeared and the remaining work
|
||||
became more difficult.</p><p>Another problem that had existed in <em>Snowboard Kids 2</em> but became more apparent as I added worktrees was synchronisation. As discussed in my <a href="https://blog.chrislewis.au/the-long-tail-of-llm-assisted-decompilation/">previous post</a>,
|
||||
each agent was given a function to decompile alongside a set of similar
|
||||
functions that had already been matched. These provided useful
|
||||
reference points for reproducing particular IDO instruction patterns.
|
||||
Work was divided between the worktrees using Nigel’s <code>--shard</code> option, which uses basic hashing to partition candidates between a specified number of workers.</p><p>This
|
||||
allowed more work to happen in parallel without duplication, but
|
||||
introduced a new problem as the worktrees diverged. One worktree, for
|
||||
example, might successfully decompile a function that was 99% similar to
|
||||
a function being attempted elsewhere, but that new reference would
|
||||
remain invisible to the other agent until the changes were merged.
|
||||
Periodically merging everything into the main branch and resynchronising
|
||||
the worktrees fixed this, but synchronising all four could take more
|
||||
than an hour. Synchronising continuously wasted time, while waiting too
|
||||
long increased drift.</p><p>To make synchronisation less critical, I
|
||||
updated the similarity search to inspect every worktree. A newly matched
|
||||
function could immediately become a reference for another agent without
|
||||
waiting for it to reach the main branch. The harness would produce
|
||||
messages such as the following.</p><div class="highlight"><pre tabindex="0" style="color: rgb(248, 248, 242); background-color: rgb(39, 40, 34); tab-size: 4; --darkreader-inline-color: var(--darkreader-text-f8f8f2, #e8e8d5); --darkreader-inline-bgcolor: var(--darkreader-background-272822, #1c1e1f);" data-darkreader-inline-color="" data-darkreader-inline-bgcolor=""><code class="language-text" data-lang="text"><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">1</span><span>✓ Candidate ["func_80094A94", "func_80094FF4 (../sbk-c)",
|
||||
</span></span><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">2</span><span> "func_80094808", "func_8009491C (../sbk-a)",
|
||||
</span></span><span style="display:flex"><span style="white-space: pre; user-select: none; margin-right: 0.4em; padding: 0px 0.4em; color: rgb(127, 127, 127); --darkreader-inline-color: var(--darkreader-text-7f7f7f, #988f81);" data-darkreader-inline-color="">3</span><span> "func_8009469C (../sbk-c)", "func_80093144"] was fixed!
|
||||
</span></span></code></pre></div><p>This helped the process remain
|
||||
efficient as the easy functions disappeared. Synchronisation was still
|
||||
needed to consolidate and push changes, but it was no longer required
|
||||
for agents to learn from one another.</p><h3 id="model-choice">Model Choice</h3><p>I
|
||||
tried GPT-5.5 and 5.6, Claude 4.5 and Fable, and GLM 5.2. This is
|
||||
completely unscientific. The models were tested against a changing set
|
||||
of difficult functions, sometimes after another model had already made
|
||||
partial progress. Broadly, though, Codex continued to outperform Claude,
|
||||
as it had towards the end of the previous project. Sol xhigh was
|
||||
particularly effective once it became available.</p><p>GLM 5.2, served
|
||||
by z.ai, was very disappointing. I had previously been a big fan of GLM.
|
||||
It was effective, even if it was not quite a frontier model, and its
|
||||
generous usage limits made up for the gap. That tradeoff became much
|
||||
less attractive as the limits grew less generous while latency remained
|
||||
awful. The feedback cycles were so long that I stopped giving it work
|
||||
and eventually cancelled my subscription.</p><h2 id="what-next">What Next?</h2><p>The
|
||||
immediate priority is to better document the game. A 100% match means
|
||||
we have C code for every function; it does not mean we understand what
|
||||
every function does. There are still generated names to replace, unknown
|
||||
structure fields to identify, awkward matches to clean up, and large
|
||||
amounts of data to describe.</p><p>Work is also underway on a <em>Snowboard Kids</em> recompilation. Fortunately, the first game shares many of the quirks addressed by patches in <a href="https://blog.chrislewis.au/snowboard-kids-2-is-recompiled/"><em>Snowboard Kids 2: Recompiled</em></a>.</p><p><img src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/snowboard-kids-recomp-screenshot_aOx2.webp" alt="early screenshot of Snowboard Kids: Recompiled"></p><p>I’m
|
||||
looking into porting the levels and other content from the first game
|
||||
into the second game’s engine, although I have no idea how much work
|
||||
that will be yet.</p><p>Beyond that, I’m interested in decompiling <em>Snowboard Kids Plus</em> on the PlayStation, a Japan-exclusive expanded release of the first game with extra levels and characters.</p><p><em>If you’ve made it this far, you’re probably interested in decompilation and Snowboard Kids. Take a look at the <a href="https://github.com/cdlewis/snowboardkids-decomp">Snowboard Kids decompilation project</a>. There is still plenty of cleanup and documentation work to do, and contributions are very welcome.</em></p><div class="footnotes" role="doc-endnotes"><hr><ol><li id="fn:1"><p>Almost.
|
||||
A small amount of handwritten assembly from system libraries remains
|
||||
because assembly was the original source rather than a temporary
|
||||
decompilation placeholder. <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:2"><p>In the best case, I think the project would have stalled around 89–90% without their IDO expertise. <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:3"><p>For <em>Snowboard Kids 2</em>, I measured from the <a href="https://github.com/cdlewis/snowboardkids2-decomp/commit/f1025d16a8aa1d11ec937f8c721af59149feee7c">first commit on 28 September 2024</a> to the <a href="https://github.com/cdlewis/snowboardkids2-decomp/commit/ec405bd1aa8a6a2fed4421e0ba33e37b973db4ec">final matching function on 17 May 2026</a>, a span of 596 days. The 84-day figure for <em>Snowboard Kids</em> is my project-level count; the public repository itself spans from the <a href="https://github.com/cdlewis/snowboardkids-decomp/commit/08f9b60604fd084215770d4a787f7fa9d4d61865">first commit on 28 May 2026</a> to the <a href="https://github.com/cdlewis/snowboardkids-decomp/commit/875002983e210f492aec61ebe78444d2227e270b">final matching function on 19 August 2026</a>, just over 83 days. <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:4"><p>Based
|
||||
on a manual review of the Git history, I identified roughly 41
|
||||
expert-assisted matches, corresponding to about 4.8% of the commits I
|
||||
classified as function-matching work. This retrospective classification
|
||||
is approximate because commits can contain multiple functions, help was
|
||||
often incorporated into my own commits, and the figure does not measure
|
||||
person-hours. Note that I am not counting myself as an “expert”. <a href="#fnref:4" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:5"><p>One notable exception was <a href="https://github.com/cdlewis/snowboardkids-decomp/blob/ba90a9ead954514cfaa1e7c0143957fdfc754092/src/menu/main_menu/controller_main_menu_flow.c#L560"><code>validateControllerPakSave</code></a>,
|
||||
which appears to have been used to debug Controller Pak saves. Most of
|
||||
its debugging and validation logic was omitted from the final
|
||||
build. <a href="#fnref:5" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:6"><p><a href="https://iwataasks.nintendo.com/interviews/wii/sinandpunishment/0/0/">Nintendo has described</a>
|
||||
adopting an architecture from Silicon Graphics for the Nintendo 64. The
|
||||
original development system was similarly SGI-centric. Nintendo’s <a href="https://ultra64.ca/files/documentation/online-manuals/man-v5-1/pro-man/pro01/index.htm">development documentation</a>
|
||||
describes connecting its emulator board to an SGI Indy workstation, and
|
||||
IDO was one of the supported compilers. The relationship therefore
|
||||
extended from the console’s underlying architecture to the machines and
|
||||
tools used to develop its games. <a href="#fnref:6" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:7"><p>IDO used a <a href="https://techpubs.jurassic.nl/library/manuals/2000/007-2479-001/sgi_html/ch01.html">multi-stage compilation pipeline</a>. Its <a href="https://techpubs.jurassic.nl/manpages_0530/cat5/uopt.html"><code>uopt</code> documentation</a>
|
||||
says that eligible loops were unrolled four times by default.
|
||||
Coincidentally, four is also the number of racers in the game. Loops
|
||||
with four iterations are therefore candidates for complete unrolling,
|
||||
and they occur frequently 🫠. <a href="#fnref:7" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:8"><p>Measured from the <a href="https://github.com/gcsmith/Pilotwings64Decomp/commit/64ae65fbc0dcbb1de772361b1dd84faf0816dc56">addition of progress tracking on 26 January 2026</a> to the <a href="https://github.com/gcsmith/Pilotwings64Decomp/commit/c041f5190e232988fec0cd17b1a5e6ad9182c25d">final matching functions on 10 April 2026</a>, the decompilation took almost exactly 74 days. <em>Pilotwings 64</em> had 1,791 functions compared with 2,145 for <em>Snowboard Kids</em>, but approximately 838 KB of code compared with 732 KB for <em>Snowboard Kids</em>. <a href="#fnref:8" class="footnote-backref" role="doc-backlink">↩︎</a></p></li><li id="fn:9"><p>Using a one-shot script like this is not a particularly novel idea. I’m pretty sure I copied it from somewhere, possibly <em>Diddy Kong Racing</em>’s <a href="https://github.com/DavidSM64/Diddy-Kong-Racing/blob/84f0ea569b07903ba8a4f9f252e8dc8e4ba54bdc/m2c_all.sh">m2c_all.sh</a>. <a href="#fnref:9" class="footnote-backref" role="doc-backlink">↩︎</a></p></li></ol></div></content><section class="bluesky-comments" data-bluesky-post="https://bsky.app/profile/did:plc:5j7epmcl35kl5pni4y6ehpl2/post/3mu34bwrh4k2m"><h2>Comments</h2><p class="bluesky-comments__status">Loading Bluesky comments...</p></section><script defer="defer" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/bluesky-comments_aOx2.js"></script><div style="font-size:.8em;display:flex;gap:16px;justify-content:center"><p><a href="https://blog.chrislewis.au/snowboard-kids-2-is-recompiled/"><< Previous Post</a></p><p>|</p><p><strike>Next Post >></strike></p></div><p></p></main><footer></footer><script type="module" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/v3d52b47920f24c319d37e2661827c42b1787588026925_aOx2" integrity="sha512-d9sL6GJLXn6fInD1+TVXhTcQOsmxeHfmHAvwGDIxp5TO+uo1fiWW7mHomMj4MLRlCsJDTqXzWLHJFFlPCEIj/A==" data-cf-beacon="{"version":"2024.11.0","token":"10d5aece9021411ab2fd8081c3b4e020","r":1}" crossorigin="anonymous"></script>
|
||||
<div>
|
||||
<div class="vtResetstyles">
|
||||
<div id="vtoverlay" class="vtoverlay w3-animate-right w3-container vtfadein">
|
||||
<div class="vtmain" id="vtmain">
|
||||
<div class="vtcontactus">
|
||||
<a id="linktest" href="https://www.virustotal.com/">
|
||||
<img class="vtlogo-popup" style="
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
margin-right: 5px !important;
|
||||
" src="Decompiling%20a%20Nintendo%2064%20Game%20in%2084%20Days%20_%20Chris'%20Blog_files/gti-vt-icon2_aOx2.png">
|
||||
</a>
|
||||
<a href="https://www.virustotal.com/">VT4Browsers + Google TI
|
||||
</a>
|
||||
<a id="vtclose" class="vtclose" style="margin-left: 15px;"><svg xmlns="http://www.w3.org/2000/svg" width="2em" height="2em" viewBox="0 0 24 24" fill="currentColor" style="vertical-align: middle">
|
||||
<path d="M6.4 18.654 5.346 17.6l5.6-5.6-5.6-5.6L6.4 5.346l5.6 5.6 5.6-5.6L18.654 6.4l-5.6 5.6 5.6 5.6-1.054 1.054-5.6-5.6-5.6 5.6Z"></path>
|
||||
</svg></a>
|
||||
</div>
|
||||
<div class="vttextpopup" id="statustext">
|
||||
<div class="vtscanstatus" id="vtscanstatus">Fetching file...<br></div>
|
||||
<div class="vtprogress" id="vtprogress"></div>
|
||||
<br id="brfile" class="vthidden">
|
||||
<div class="vtscanstatus vthidden" id="vtscanfilelink"></div>
|
||||
</div>
|
||||
<div class="vttextpopup vthidden" id="widgettext">
|
||||
<div id="vtwidgetError"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vt-augment-container" class="vtzindex vt-augment drawer" style="background: rgb(49, 61, 90); --darkreader-inline-bgcolor: var(--darkreader-background-313d5a, #273148); --darkreader-inline-bgimage: none;" data-darkreader-inline-bgcolor="" data-darkreader-inline-bgimage=""></div></div><span class="vttooltiptext vthidden"> Click to open IoC report <br>with VirusTotal Augment</span><style></style><style class="darkreader darkreader--sync" media="screen"></style></body></html>
|
||||
Reference in new issue
Block a user