forked from Ports/goldeneye-pc-port
Folds in the zero-code-risk ADOPT items from scratchpad/GEVR-TRIAGE.md (a prior survey of the sibling GEVR VR port, same n64decomp/007 lineage, reference-only): a crash-triage heuristic for non-canonical x86-64 pointer faults, two frame-rate-assumption notes that pre-empt a future uncapped/high-refresh rabbit hole, a process lesson about grep-built site lists standing in for a real read, and the portal-scissor site map as pre-flight for whenever widescreen work resumes. Claude-Session: https://claude.ai/code/session_01KwZViAFaXua3PKgHHjNgvi
165 lines
8.6 KiB
Markdown
165 lines
8.6 KiB
Markdown
---
|
|
title: Development Process
|
|
description: The investigation workflow behind the port; task budgets, file partitioning, and the finding-log discipline used across sessions and agents.
|
|
---
|
|
|
|
## How this port is developed
|
|
|
|
Most of this port's work is not writing code; it is **diagnosis**: figuring
|
|
out which N64 hardware or ABI behavior a piece of unmodified game code is
|
|
silently relying on, and satisfying it in the `port/` layer without touching
|
|
the game's logic. A lot of that diagnosis is done with LLM coding agents, and
|
|
the workflow below is what makes that productive rather than chaotic.
|
|
|
|
For the project context (the two-model split, the timeline, and an honest
|
|
assessment of what did and did not work), see
|
|
[`dev/agentic-development.md`](dev/agentic-development.md).
|
|
|
|
It is adapted from Chris Lewis's write-up
|
|
[*"Decompiling a Nintendo 64 Game in 84 Days"*](https://blog.chrislewis.au/decompiling-a-nintendo-64-game-in-84-days/)
|
|
(Snowboard Kids, 2026), an AI-assisted decompilation that ran several times
|
|
faster than its predecessor. That project bit-matches compiler output;
|
|
this one matches runtime behavior, so the mechanics differ; but the
|
|
project-structure lessons carry over directly.
|
|
|
|
## 1. Every investigation task gets a visible budget
|
|
|
|
An open-ended tool; for them the permuter, for us "rebuild and run the game
|
|
and stare at a frame dump"; wrecks throughput when an agent will grind it
|
|
indefinitely. Giving the agent a **deadline it can see** lets it trade
|
|
tool-time against thinking and decide when to give up.
|
|
|
|
So every investigation brief states:
|
|
|
|
- a **budget**; "≤ N build → run → inspect cycles" or "~M minutes"; and
|
|
- the **fallback on expiry**: stop, revert any temporary probes, and write up
|
|
what was found with an explicit confidence rating. A good write-up of a
|
|
half-solved bug is a deliverable, not a failure.
|
|
|
|
The budget scales with the project's maturity. Early bugs are cheap (one-line
|
|
truncations, off-by-ones) and get a tight cap; once those are gone the
|
|
survivors are structural (matrix handedness, serialization formats) and get
|
|
more room.
|
|
|
|
## 2. A shared, append-only learnings file
|
|
|
|
Lewis's agents recorded generalisable IDO quirks in a learnings file; later
|
|
agents read it and did better. Our equivalent is
|
|
[`porting-notes.md`](porting-notes.md); the recurring N64→PC bug classes,
|
|
each entry a terse index into the full
|
|
[`dev/findings.md`](https://github.com/jkdansereau/goldeneye-pc-port/blob/main/docs/dev/findings.md)
|
|
log. Every investigation brief links it, and every investigation ends by
|
|
appending any new generalisable quirk.
|
|
|
|
The classes it currently tracks:
|
|
|
|
- pointer-width struct growth (32→64) in ROM-serialized or pun-allocated
|
|
structs; the dominant class;
|
|
- 16-byte host `Gfx`/`Vtx` vs 8-byte N64; reservation and copy sizing;
|
|
- big-endian rodata read on a little-endian host: `f32` word-pairs, header
|
|
offset tables, packed bitfields;
|
|
- N64 hardware idioms the software RSP does not emulate (fill-rect Z clear,
|
|
LOD/detail tiles, segment-address folds);
|
|
- whole serialized formats are converted by an **offline sidecar** rather than
|
|
patched at runtime.
|
|
|
|
**Two process lessons from the sibling GEVR VR port** (same decomp lineage,
|
|
reference-only survey in `scratchpad/GEVR-TRIAGE.md` item D — their corpus
|
|
hit both repeatedly enough to name them): (1) **a grep standing in for a
|
|
read** — a site list built by grepping one helper name or spelling misses
|
|
siblings spelled differently or living in an adjacent file (their own
|
|
missed-smoke-scissor-site example); treat a grep-built site list as a draft,
|
|
not a census, until it's been read against. (2) **write the falsifier before
|
|
the run** — state up front what result would kill the hypothesis ("if X
|
|
survives, hypothesis Y is dead"), not just what would confirm it; this fits
|
|
the existing budget/findings discipline directly and costs one sentence per
|
|
brief.
|
|
|
|
## 3. Parallelise by file, consolidate often
|
|
|
|
Lewis ran multiple git worktrees and found the real cost was
|
|
*synchronisation*. For parallel investigation agents the same rule applies:
|
|
|
|
- **Partition tracks by the files they will touch**, so patches never collide.
|
|
A task that can't be cleanly file-partitioned shouldn't be parallelised.
|
|
- Each agent reverts its own temporary probes before reporting, leaving only
|
|
committed, env-gated, capped diagnostics; and never touches another track's
|
|
uncommitted edits.
|
|
- One integrator merges each track with its write-up, then the next round
|
|
starts from a clean tree. Investigation branches are not allowed to drift.
|
|
|
|
## 4. Spend the cheap automated pass before spending agent time
|
|
|
|
Before dispatching an investigation agent:
|
|
|
|
- confirm the build is green and the link is clean;
|
|
- run the existing env-gated probes for the area and attach their output;
|
|
- capture a baseline frame dump;
|
|
- grep the finding log for a prior instance of the same bug class;
|
|
- check the [Perfect Dark port](https://github.com/fgsfdsfgs/perfect_dark) for
|
|
the analogous code; same engine family.
|
|
|
|
Then hand the agent the *findings*, not just the problem.
|
|
|
|
## 5. What does not transfer from the decomp workflow
|
|
|
|
- **The match-percentage loop.** This port doesn't bit-match. "Done" is: no
|
|
fault, and visually correct against N64 reference footage.
|
|
- **"Understanding the function is the easy part."** True when decompiling;
|
|
inverted here; the hard part is identifying the implicit hardware/ABI
|
|
contract the already-readable C depends on.
|
|
|
|
## 6. Keep PR/git-history noise low
|
|
|
|
A session produces a lot of small, independently-verifiable units of work;
|
|
one Dxx row, one docs pass, one mechanical fix. Left unmanaged that turns
|
|
into a pile of small open PRs that's harder to review than the work
|
|
justifies. Rules:
|
|
|
|
- **Squash-merge single-purpose and docs-only PRs.** The repo has squash
|
|
merge enabled; use it (`gh pr merge --squash --delete-branch`) for anything
|
|
whose in-branch commit history (iteration, probe-add, probe-strip) isn't
|
|
itself useful to keep; which is most PRs. Reserve a plain merge commit for
|
|
a PR whose individual commits are each a distinct, reviewable unit someone
|
|
might want to `git revert` independently (e.g. this session's PR #50: a
|
|
fix commit + a separate analysis-only commit).
|
|
- **Batch same-session, same-flavor docs updates into one PR/branch**
|
|
instead of opening a new PR per finding row. A runtime re-check that
|
|
updates one `findings.md` row (like #48) doesn't need its own PR if
|
|
another docs-only branch is already open in the same session; fold it in.
|
|
Only split into separate PRs when the pieces have genuinely different
|
|
review/merge timing (e.g. one needs a human playtest, the other doesn't).
|
|
- **A branch takes only the change it's named for.** Don't let unrelated
|
|
housekeeping (a stray gitignore fix, a config tweak) ride along on an
|
|
active feature/fix branch just because it's the one checked out; cut a
|
|
separate branch, even for a one-line change.
|
|
- **Stacked PRs declare the dependency in the title/body** ("step 2 of N,
|
|
step 1 is #N") and get rebased/merged in order promptly; don't let a
|
|
later step sit open so long the earlier step's content drifts under it.
|
|
- **Triage the open-PR list at least once a session**, ordered by
|
|
time-to-close (docs-only and non-draft first, human-playtest-gated last).
|
|
A PR idle long enough that `main` has moved past the finding rows it
|
|
touches is **stale, not just old**; check for real textual conflict
|
|
(`git merge-tree`), not just calendar age. If `main` already has more
|
|
current information than the PR (a bug the PR still lists `OPEN` that's
|
|
since been fixed and verified), **close it rather than force a merge or
|
|
spend a cycle rebasing it**; re-derive only whatever part of its content
|
|
is still true as a fresh, small PR against current `main`. Merging stale
|
|
content back over newer information is a regression, not a save.
|
|
|
|
## Investigation-brief template
|
|
|
|
```
|
|
TASK: <one bug, one subsystem>.
|
|
READ FIRST: docs/porting-notes.md; docs/dev/findings.md <specific Dxx entries>.
|
|
FILES YOU MAY TOUCH: <disjoint from any other in-flight work>.
|
|
KNOWN-GOOD / RULED OUT: <list; do not re-investigate>.
|
|
PRE-FLIGHT ATTACHED: <probe output, baseline capture, 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 the finding log.
|
|
VERIFY: <the exact tools_pc/verify.sh (or probe) invocation that proves this done; or, if runtime verification is impossible in this environment, say so and name what a human must run>.
|
|
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/porting-notes.md.
|
|
```
|