forked from Ports/goldeneye-pc-port
Merge pull request #7 from jkdansereau/docs/dropin-release-plan-and-status
Harness: Local Qwen 3.8 dispatch
This commit is contained in:
commit
c346f5c18c
8 files changed
+426
-19
No files matched your search
@@ -5,7 +5,10 @@ on:
|
||||
branches: [ main, master ]
|
||||
tags: [ 'v*' ]
|
||||
# No paths-ignore here: tag pushes must always build so a release can be
|
||||
# cut even when the tagged commit only touched docs. PRs keep the filter.
|
||||
# cut even when the tagged commit only touched docs, and a workflow that
|
||||
# never triggers would leave a required status check pending forever.
|
||||
# Docs-only branch pushes are instead skipped by the `changes` job below,
|
||||
# which reports the build jobs as "skipped" (green for branch protection).
|
||||
pull_request:
|
||||
paths-ignore: [ '**.md', 'docs/**', 'LICENSE', 'NOTICE' ]
|
||||
workflow_dispatch:
|
||||
@@ -42,7 +45,42 @@ concurrency:
|
||||
# only job granted contents: write.
|
||||
|
||||
jobs:
|
||||
# Did this push touch anything outside docs / licenses? Branch pushes to main
|
||||
# get no workflow-level paths-ignore (see the `on:` comment), so this is where
|
||||
# a docs-only merge is filtered out. Tags and pull_request events always build.
|
||||
# Mirrors the pull_request paths-ignore globs: **.md, docs/**, LICENSE, NOTICE.
|
||||
changes:
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
code: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- id: filter
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" != "push" ] || [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"; exit 0
|
||||
fi
|
||||
base='${{ github.event.before }}'
|
||||
if ! git cat-file -e "${base}^{commit}" 2>/dev/null; then
|
||||
echo "no usable base ${base} — building to be safe"
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"; exit 0
|
||||
fi
|
||||
changed="$(git diff --name-only "$base" '${{ github.sha }}')"
|
||||
echo "Changed files:"; echo "$changed"
|
||||
if echo "$changed" | grep -qvE '(\.md$|^docs/|^LICENSE$|^NOTICE$)'; then
|
||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "docs-only push — skipping build"
|
||||
echo "code=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
validate:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -78,6 +116,8 @@ jobs:
|
||||
# address-constant romassets_<region>.s. The ROM (.z64) and the ROM-derived
|
||||
# blobs are runtime-only dependencies — CI cannot run the game, only build it.
|
||||
windows-build:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
runs-on: windows-latest
|
||||
defaults:
|
||||
run:
|
||||
@@ -139,7 +179,7 @@ jobs:
|
||||
# gets contents: write.
|
||||
release:
|
||||
if: github.ref_type == 'tag'
|
||||
needs: [ validate, windows-build ]
|
||||
needs: [ changes, validate, windows-build ]
|
||||
runs-on: ubuntu-22.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -66,3 +66,28 @@ Full template: `docs/dev-process.md` -> "Investigation-brief template".
|
||||
- **Per batch / integration checkpoint:** one 60s `-level_09` crash-free run.
|
||||
- Subagents on small disjoint subsystems (input, audio, saves, converters)
|
||||
don't run the game — the overseer verifies.
|
||||
|
||||
## Local Qwen dispatch (cost-free, data-local worker)
|
||||
|
||||
A local Qwen3.8-27B is wired in via the `delegate-local` MCP server (see
|
||||
`C:\Users\james\Source\Repos\20260902-qwen38claudepair`). Use it as a
|
||||
*subagent whose compute runs on-machine* for scoped, checkable work — the lead
|
||||
still owns judgment, planning, and the verification gate.
|
||||
|
||||
- **Tools:** `local_backend_status()` (run once before first dispatch),
|
||||
`list_local_agents()`, `delegate_to_local_agent(agent_name, task, workdir, max_turns?, max_tokens?)`.
|
||||
- **Workers** (`~/.claude/agents/`): `repo-mapper` (read-only ingest/map),
|
||||
`mechanical-coder`, `test-writer`, `triage` (read-only root-cause + patch),
|
||||
`refactor-bot`.
|
||||
- **Dispatch when ALL hold:** narrow blast radius, clear spec, output checkable
|
||||
by a build/test/diff. Keep on Claude: ABI/handedness/format reasoning, novel
|
||||
debugging, anything you can't cheaply verify.
|
||||
- **Backend reality:** ~1 inference slot, slow per token — dispatch
|
||||
**sequentially**, `max_tokens` ~4–8K, treat as batch work. Needs the LiteLLM
|
||||
proxy (`litellm/run.ps1`) + Unsloth Studio both up.
|
||||
- **Windows quirk:** the worker's shell is `cmd.exe`, not bash — its agent
|
||||
prompt tells it to use `read_file`/`findstr`, not `cat`/`grep`. Always
|
||||
spot-check the returned artifact against the real files before integrating;
|
||||
the 27B will confidently fabricate if a read fails.
|
||||
- Fits the **dispatch preflight** rules above (FILES / BUDGET / ON EXPIRY /
|
||||
CONSTRAINTS / REPORT) — write the `task` string to that shape.
|
||||
@@ -187,13 +187,13 @@ the front end into the early game. It is not finished and it is not stable.
|
||||
|
||||
## Download
|
||||
|
||||
Pre-built **Windows x86-64** bundles are published under
|
||||
[Releases](../../releases) as marked pre-releases — the engine executable plus
|
||||
its runtime DLLs, no ROM and no game assets. You still supply your own ROM (see
|
||||
[Requirements](#requirements)) and drop it in `data/` as described in the
|
||||
bundled README. Every download is a research alpha with the caveats in
|
||||
[Status](#status) above; there is no stable build yet. To build from source
|
||||
instead, see [Building](#building).
|
||||
There are no pre-built binaries yet. The project is Phase 2 WIP with the
|
||||
caveats in [Status](#status) above — nothing runs end to end — so the only way
|
||||
to try it is to build it yourself; see [Building](#building). You supply your
|
||||
own ROM (see [Requirements](#requirements)) and drop it in `data/` before
|
||||
building. Pre-release **Windows x86-64** bundles (engine executable plus
|
||||
runtime DLLs, no ROM or game assets) may be published under
|
||||
[Releases](../../releases) once the port is playable.
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -388,19 +388,19 @@ GitHub. It follows the same conventions they do:
|
||||
|
||||
- **No ROM and no game assets are distributed** — not in this repository and
|
||||
not in any release. Textures, audio, models, level data and in-game text are
|
||||
extracted from a ROM *you already own*, on *your* machine (at build time for
|
||||
a source build; at first run for a pre-built binary).
|
||||
extracted from a ROM *you already own*, on *your* machine, at build time.
|
||||
- The repository is a fork of the public
|
||||
[GoldenEye 007 decompilation](https://github.com/n64decomp/007) and inherits
|
||||
its contents unmodified (see [`NOTICE`](NOTICE) for what that includes).
|
||||
- No official logos, box art, or marketing assets are used. "GoldenEye 007",
|
||||
"007", "James Bond" and related marks belong to their respective owners
|
||||
(Nintendo, Microsoft/Rare, MGM, Danjaq, EON Productions).
|
||||
- **Pre-built binaries** offered under [Releases](../../releases) contain only
|
||||
the engine — the `port/` layer plus the compiled decompilation, with no game
|
||||
data of any kind — bundled with permissively-licensed runtime libraries
|
||||
(SDL2, zlib, the MinGW runtime; their licenses travel in the download). Any
|
||||
build, yours or ours, is useless without a ROM you supply.
|
||||
- **If** pre-built binaries are published under [Releases](../../releases) in
|
||||
the future, they will contain only the engine — the `port/` layer plus the
|
||||
compiled decompilation, with no game data of any kind — bundled with
|
||||
permissively-licensed runtime libraries (SDL2, zlib, the MinGW runtime; their
|
||||
licenses travel in the download). No such release exists yet, and any build —
|
||||
yours or ours — is useless without a ROM you supply.
|
||||
|
||||
This project is **not affiliated with, endorsed by, or sponsored by** Nintendo,
|
||||
Rare, Microsoft, MGM, Danjaq, EON Productions, or any rights holder in
|
||||
|
||||
@@ -250,4 +250,39 @@
|
||||
.page-header { padding: 3rem 1rem 2.75rem; }
|
||||
.main-content { padding-top: 2rem; }
|
||||
}
|
||||
|
||||
/* ---- Mermaid diagrams ------------------------------------------------ */
|
||||
/* Kramdown renders ```mermaid fences as <pre><code class="language-mermaid">.
|
||||
The script below hands their text to mermaid and swaps in the SVG. Keep
|
||||
the pre unstyled (no dark code background) while it waits to render. */
|
||||
.main-content pre.mermaid,
|
||||
.main-content pre > code.language-mermaid {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: var(--ink);
|
||||
padding: 0;
|
||||
}
|
||||
.main-content pre.mermaid { text-align: center; overflow-x: auto; }
|
||||
.main-content pre.mermaid:not([data-processed]) { visibility: hidden; }
|
||||
</style>
|
||||
|
||||
<script type="module">
|
||||
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
|
||||
|
||||
// Move each ```mermaid code block's text into a <pre class="mermaid"> that
|
||||
// mermaid.run() recognises.
|
||||
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
document.querySelectorAll("pre > code.language-mermaid").forEach((code) => {
|
||||
const pre = document.createElement("pre");
|
||||
pre.className = "mermaid";
|
||||
pre.textContent = code.textContent;
|
||||
code.parentElement.replaceWith(pre);
|
||||
});
|
||||
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: "loose", // our own docs; needed for <br/> in node labels
|
||||
theme: dark ? "dark" : "neutral",
|
||||
});
|
||||
mermaid.run({ querySelector: "pre.mermaid" });
|
||||
</script>
|
||||
@@ -3,7 +3,7 @@
|
||||
*A case study in AI-agent collaboration on a large, low-level codebase. A
|
||||
GoldenEye 007 Nintendo 64 → desktop port, taken from "builds a ROM" to
|
||||
"playable front end" in about two weeks — driven mostly by two coding agents (a
|
||||
local open-weight model, Qwen 3, on a single RTX 5090, and a hosted frontier
|
||||
local open-weight model, Qwen 3.8, on a single RTX 5090, and a hosted frontier
|
||||
model, Claude / Claude Code), handing work back and forth through shared
|
||||
written notes under one person's part-time direction. This is the goal, the
|
||||
method, the timeline, and an honest read on what did and didn't work.*
|
||||
|
||||
+64
-1
@@ -392,7 +392,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D169 | **Front-end mouse pointer can't reach the outer cells of the mission/level-select grid — only an inner ~3×3 selectable (M-33, developer bug report).** The D165 pointer P-controller in `port/src/input.c` is hard-coded to a **320×240** virtual field (`MENU_CURSOR_HI_H`/`_HI_V` = 300/220, `MENU_CURSOR_MID_H`/`_MID_V` = 160/120, `input.c:145-149`), but GE's front end runs at **440×330** (`front.c:8570` `viSetViewSize(440,330)`; default cursor home 220/165 `front.c:285`). `frontUpdateControlStickPosition` clamps the real cursor to `[20,420]×[20,310]` (`front.c:1195-1217`); the mission-select hit-test grid spans x 73…352 / y 62…270 with outer split points 317 / 235.5 (`front.c:516,519,3180,3194`). The port clamps its pointer *target* and *estimate* to 300/220 (`input.c:553-564`) → the stick zeroes out once both saturate → the game's `cursor_h_pos`/`cursor_v_pos` park at ≈300/220, short of the two outer columns and the bottom row. File-select / mode-select / main-menu are unaffected because their hit targets (centred folder boxes, the `x=126` mode list, the `x=106` difficulty list) all sit inside the 320×240 sub-box. Not the D118d `joyGetStickY`-threshold class — this is a virtual-resolution constant mismatch, closer to D164 (a front-end layout constant wrong on the PC path). Also note: the "re-syncs whenever the target is held at a screen edge" comment at `input.c:141-143` describes behaviour the code doesn't implement — the only estimator reset is the activation re-home (`input.c:545-549`), so once pinned at the clamp the estimate never recovers. Confidence: **medium-high** (constants + clamp math unambiguous in source; static-only, no build/run this session; exact reachable block "~4×3" vs the reported "3×3" within tolerance). | **FIXED (M-33, `port/src/input.c`, port-only, no `#ifdef PORT`).** The `menuMode` pointer branch now derives its clamp bounds from the live virtual screen — `[screenleft+20, screenleft+screenwidth-20] × [screentop+20, screentop+screenheight-20]` via `getPlayer_c_screenwidth/height/left/top()` (`src/game/bondview.c:880-895`) — falling back to the old 320×240 constants when the front-end screen isn't set (`sw` outside 200…2000). The estimator seed on activation is now the real `cursor_h_pos`/`cursor_v_pos` (`front.c:285`, externed in `front.h`) instead of the 160/120 centre guess. `MENU_CURSOR_HI_H/_V` / `MID_H/_V` kept only as the fallback. **Verified:** builds + links clean (`getPlayer_c_screen*` and `cursor_[hv]_pos` all resolve — non-static engine symbols, as expected); `GE_STARTMENU=7` mission-select boots crash-free 600+ frames; `-level_09` unregressed (framediff 3/3 within threshold, 91.6% nonclear). The in-level path is untouched (menu-only branch). **Interactive feel-check still owed** — headless input can't drive the mouse pointer, so "every grid tile is now reachable" is inferred from the corrected clamp math, not observed. `Input.MenuPointerMode=0` (legacy velocity) and the constant fallback remain as escape hatches. |
|
||||
| D178 | **Pre-mission briefing screen: objectives blank / missing, briefing pages blank (also the D143 "briefing text blank" side effect) — FIXED (M-36).** Root cause: the briefing segment (`Ubrief*Z`, source `assets/obseg/brief/*.c`) is a raw ROM image of `struct BriefStruct` = `{ u16 brief[4]; struct { u16 textid; u16 enabled_difficulty; } objective[10]; }` (48 bytes), loaded by `front.c load_briefing_text_for_stage()` via `_fileNameLoadToAddr()` — **no converter and no BE→LE fixup anywhere in the chain**. On the LE host all 24 `u16` read byte-swapped. Measured on Dam (`GE_D178=1`): raw `brief=002c,012c,022c,032c obj0=042c/0100 obj3=072c/0000` vs correct `2c00,2c01,2c02,2c03 / 2c04/0001 / 2c07/0000`. Two independent symptoms follow: (a) `textid` `0x2C04` (= `getStringID(LDAM,4)`, bank 11 slot 4) reads as `0x042C` → bank 1 slot 44, never loaded → `langGet()` NULL → **blank text** (this is exactly the D143 residual, `front.c:6732` and `brief[0..3]` at `front.c:6855-6867`); (b) `enabled_difficulty` `0x0001` (Secret Agent) reads as `0x0100 = 256`, so `selected_difficulty >= enabled_difficulty` (`front.c:6729`) is false for every difficulty-gated objective — on Agent only the one `DIFFICULTY_AGENT`(0) objective survived the filter, which is why a single bare "a." bullet printed. **Fix:** `romdataFixupBriefing()` in `port/src/romdata.c` (+ `port/include/romdata.h`), called from a `#ifdef PORT` block in `load_briefing_text_for_stage()` right after the load — the same shape as `langFixupLoadedBank()` in `language.c` (BE-serialized-struct reconciliation, semantics-preserving, no game logic touched). `GE_D178=1` prints the raw and fixed words. **Verified:** Dam briefing on Agent now shows "a. Bungee jump from platform"; on `GE_STARTMENU_DIFF=3` (00 Agent) all four (Neutralize all alarms / Install covert modem / Intercept data backup / Bungee jump from platform), correct difficulty gating. `-level_09` framediff 3/3; `GE_STARTMENU=7`/`=13` crash-free. | **FIXED (M-36)** — high confidence |
|
||||
| D175 | **In-game stutter / brief hang during normal play** (user QA report): opening a door on Runway (`-level_35`, mission 3) and also observed on Surface. Self-recovered; no backtrace captured. Likely one of the known transient-hang classes (D155 catch-up spiral / D156 anim NaN loop / D134 task-done event / D147-D152 audio-lock steal) or a benign new-room texture-import frame spike on door open. See `GRAPHICS-BACKLOG.md` D175 for the gdb pattern-match sheet. | Observed, not investigated |
|
||||
| D176 | **Surface exterior renders wrong (`-level_36`), two independent defects.** **(a)** sky solid black — env data is correct (`Clouds=1`, warm `CloudRGB`), the cloud-sky path runs, but `skyRenderTri`/`skyRenderFull` emit nothing → see "D176(a)" below, partial, not root-caused. **(b)** cliff/rock walls = grey diagonal static — NOT a texture decode bug (D183 disproved the shear hypothesis, 0/166 loads strided); re-scope from ROM ground truth of the wall texnum. See `GRAPHICS-BACKLOG.md` D176. | (a) partial / (b) open — both M-36 |
|
||||
| D176 | **Surface exterior renders wrong (`-level_36`), two independent defects.** **(a)** sky solid black — env data is correct (`Clouds=1`, warm `CloudRGB`), the cloud-sky path runs, but `skyRenderTri`/`skyRenderFull` emit only `G_RDPHALF_*` immediates which `gfx_pc.cpp:2901` deliberately no-ops → **root-caused M-37** (see "D176(a) — M-37 UPDATE"); fix = decode the RDPHALF sky-tri stream in fast3d. **(b)** cliff/rock walls = grey diagonal static — NOT a texture decode bug (D183 disproved the shear hypothesis, 0/166 loads strided); re-scope from ROM ground truth of the wall texnum (M-37: inconclusive, leaning tiling-density). See `GRAPHICS-BACKLOG.md` D176. | (a) root-caused, fix owed / (b) open |
|
||||
| D177 | **Ladders non-functional — progression blocker (user QA report) — FIXED (M-36).** Not the input path and not the ladder state machine: `MoveBond` gates the ladder-collision path on `stanGetLocusCount(&curLocus)`, which was pinned at 0 on PC. `stanCheckLinkedSpecialTile` writes the LADDER signal via raw `outFlags[1] = 1` into a `struct StandTileLocusCallbackRecord` whose first member `s32 *rooms` is pointer-widened on PC → `[1]` is the high half of `rooms`, and `count` (moved +4→+8) is never written. Compounded by `curLocus` being declared as the 8-byte placeholder `move_bond_temp_struct` (too small for the widened record) and a `(s32)coords` pointer-truncation AV waiting in `stanGetMoveBondCollisionTiles`. **Fix:** `#ifdef PORT` — write record fields by name, declare `curLocus` as the real struct, `PORT_PTRADD` for the truncating cast (`src/game/stan.c`, `src/game/bondview2.c`; no game logic). Full detail: §F "D177". | **FIXED (M-36)** — high confidence; interactive climb test owed |
|
||||
| D174 | **"No blood effect" (user QA report).** Likely the unverified D120 decal fix: `d43_emit.py` now emits the opcode-0x18 `PointUsage[]` chain, but it was never interactively verified and requires a full sidecar regen to take effect (`debug.ps1` does not regen). Spray path is D172 (draws, wrong colour) — total absence would be new. See `GRAPHICS-BACKLOG.md` D174. | Observed; first step = BUNKER1 firefight with regenerated sidecars |
|
||||
| D183 | **M-36 Family A ("texture line/pitch shear") is DISPROVEN for the Surface repro (`-level_36`).** The importers' pitch assumption is real but never fires there; the Surface cliff "grey static" is not a shear. Full evidence + what the walls actually are: §F "D183" below. A defensive, provably-no-op de-stride landed in `import_texture()` anyway (covers the `gfx_dp_load_tile` case the `SUPPORT_CHECK`s assert against), plus `GE_DTEX` STRIDED marker + `GE_TEXRAW` raw-source dump. | Hypothesis DISPROVEN + diagnostics shipped; D176(b)/D182(2) still OPEN |
|
||||
@@ -4556,6 +4556,24 @@ Remaining candidates, in order:
|
||||
still be a real `gfx_dp_load_tile` strided case (front-end `texLoadFromGdl`
|
||||
paths do use windowed loads). The defensive fix in §5 would cover it if so.
|
||||
|
||||
**M-37 UPDATE (inconclusive, ~8-min timebox):** the §7 offline decoder + logs
|
||||
live *outside* this checkout — a full offline ROM decode was not achievable.
|
||||
Static findings: Surface's bg is `bg/bg_sevx_all_p.seg` (`bg.c:201`); the wall
|
||||
texture is a `u16` image ID *embedded in the bg GDL stream*, not a static
|
||||
per-level texnum list, so it cannot be grepped — needs a runtime `GE_DTEX`/
|
||||
`GE_TEXRAW` correlation. `assets/images.def` has no rock/cliff names (only
|
||||
`STATIC_NOISE 0x389`); Surface-named entries are mission-select thumbnails.
|
||||
Leaning **"real texture / tiling-density" (candidate 2), low-med confidence** —
|
||||
D183's own measurement (constant full alpha 0x0F + full-range noise intensity,
|
||||
zero residual structure at any pitch) is the signature of an intentional grey
|
||||
rock-detail tile, not a mis-decoded structured image. Decisive next steps:
|
||||
(i) grep the *converted* `bg_sevx_all_p` GDL for opcode `0xC0` (`G_SETTEX`) —
|
||||
settles the one remaining "wrong bind" scenario (stale tile state, TEXTURE-
|
||||
GLITCH-ANALYSIS §2 RC1) offline; (ii) headless `-level_36` `GE_DTEX/TEXRAW=1`
|
||||
logging the image ID bound to the cliff tris, look it up in `g_Textures[]`,
|
||||
read + decode that ROM range per §4. Scratch note: M-37 session scratchpad
|
||||
`D176b-rom-groundtruth.md`.
|
||||
|
||||
### 5. What shipped (`port/fast3d/gfx_pc.cpp` only)
|
||||
|
||||
- **De-stride in `import_texture()`**, before the format dispatch: when
|
||||
@@ -4636,6 +4654,51 @@ sunset cloud gradient. Headless `-level_36` + `GE_D176=1` probe
|
||||
`src/game/bgfog.c` + `src/game/sky.c`. Separate from D176(b) (the tree/rock
|
||||
"grey static", Family A / a different investigation).
|
||||
|
||||
### D176(a) — M-37 UPDATE: ROOT-CAUSED (static analysis, high confidence)
|
||||
|
||||
The emit path is not "wrong" — **the PC software RSP deliberately discards it.**
|
||||
`port/fast3d/gfx_pc.cpp:2901`:
|
||||
|
||||
```cpp
|
||||
case (uint8_t)G_RDPHALF_1:
|
||||
case (uint8_t)G_RDPHALF_2:
|
||||
case (uint8_t)G_RDPHALF_CONT:
|
||||
// on N64 skyRender uses these to render some types of skies and skybox water
|
||||
// by issuing low-level ucode commands G_TRI_FILL and G_TRI_SHADE_TXTR
|
||||
// the port renders the sky in a different manner
|
||||
break;
|
||||
```
|
||||
|
||||
`skyRenderTri` / `skyRenderFull` build their geometry **exclusively** as
|
||||
`gImmp1(gdl++, G_RDPHALF_1 / G_RDPHALF_CONT / G_RDPHALF_2, …)` pairs — a bespoke
|
||||
packed triangle-raster stream (command byte in bits 24–31 = `G_TRI_SHADE_TXTR`/
|
||||
`G_TRI_FILL`, `0x00800000` backface flag, subpixel Y coords, then S15.16 edge X +
|
||||
dX/dY slopes via `sub_GAME_7F094298`, then per-vertex RGBA shade + S/T/W tex-coord
|
||||
gradients, `G_RDPHALF_2` terminator) that GE's modified RSP ucode (`gmain.s`)
|
||||
interprets. There is **no** `gSPVertex`/`gSP*Triangle` fallback and **no**
|
||||
fill-rect on the textured case-12 branch — only `viSetFillColor(env->RGB)` sets
|
||||
FILL state that nothing consumes. → dropped → solid black.
|
||||
|
||||
Explains every prior observation: `!Clouds` levels use a plain `gDPFillRectangle`
|
||||
(→ work today); `Clouds` levels (Surface, Statue `-level_22`, Frigate `-level_29`,
|
||||
Dam exterior) go through the RDPHALF stream (→ black). The projector
|
||||
`sub_GAME_7F097388` is pure float math off endian-clean matrices — not the bug.
|
||||
|
||||
**No PD-port shortcut:** `pd_port/port/fast3d/gfx_pc.cpp:2512` has the identical
|
||||
no-op + comment and PD's `sky.c` emits the same stream; no port-side sky
|
||||
replacement exists anywhere in the PD tree. The comment is aspirational in both.
|
||||
|
||||
**Fix (Phase 2, ~1 session, genuine new fast3d code):** decode the RDPHALF stream
|
||||
in `gfx_pc.cpp` — accumulate words, on a `G_RDPHALF_1` carrying a tri opcode start
|
||||
a primitive, synthesise screen-space `LoadedVertex[3]` (bypass model/proj
|
||||
transform like the 2D `gSPTextureRectangle` path; screen XY = value/4 − viewport;
|
||||
shade + ST from integrating the slopes over the vertex delta), rasterise with the
|
||||
current combiner/tile, close on `G_RDPHALF_2`. Full stream spec + the alternative
|
||||
`#ifdef PORT` sky.c option in the M-37 scratch note.
|
||||
|
||||
Cheap first step: headless `-level_22` / `-level_29` — confirm also black (proves
|
||||
emit path, not a Surface asset).
|
||||
|
||||
---
|
||||
|
||||
## D177 — Ladders non-functional: `count`/`rooms` land in the high half of a widened pointer (M-36)
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
# Release plan — "drop in your ROM and play"
|
||||
|
||||
Status: proposal / not yet scheduled. Owner: TBD.
|
||||
|
||||
## Goal
|
||||
|
||||
An end user downloads the GitHub release zip, unzips it, drops their
|
||||
legally-owned GoldenEye 007 N64 ROM (`.z64`, big-endian) into a folder,
|
||||
double-clicks the exe, and plays. No Python, no MSYS2, no manual
|
||||
asset-extraction, no command line.
|
||||
|
||||
## Where we are today
|
||||
|
||||
The Windows bundle produced by `tools_pc/bundle-win.sh` (and uploaded by the
|
||||
`windows-build` CI job) already contains only: the engine exe, its MinGW /
|
||||
SDL2 / zlib DLLs, a README, and licenses. It deliberately ships **no** ROM and
|
||||
**no** game assets. That part is fine.
|
||||
|
||||
The gap is everything the runtime needs *alongside* the ROM before a level
|
||||
will render:
|
||||
|
||||
| Dependency | Produced today by | Consumed at runtime by |
|
||||
|---|---|---|
|
||||
| `port/src/romassets_<region>.s` — absolute cart-address symbols for every obseg/ramrom/music asset + `ge007.ld` segment markers | `python3 scripts/gen_romassets.py <u\|e\|j>`, compiled **into** the exe | linker; `&symbol` + `romCopy()` in game code |
|
||||
| `data/pcmodels-<region>/{pcmodels.bin,manifest.csv}` (~1.3 MB) — every model file re-laid-out to PC struct layout, RZ-compressed | `python3 tools_pc/d43_emit.py <region>` | `port/src/pcmodels.c` (`pcmodelsReserveSize`/`LoadSidecars`/`PatchTable`) |
|
||||
| `data/pccg-<region>/{pccg.bin,manifest.csv}` (~3.6 MB) — every stage `bg/*.seg` + `Tbg_*_stanZ` re-laid-out to PC layout | `python3 tools_pc/d69_emit.py <region>` | `port/src/pccg.c` |
|
||||
| ROM present at `data/ge007.<region>.z64` (or `baserom.<r>.z64`) | user copies it | `port/src/romdata.c` maps it at `0x10000000` |
|
||||
|
||||
Without the two sidecar trees the game shows the intro logos and then crashes
|
||||
in `modelPromoteNodeOffsetsToPointers` (finding D179).
|
||||
|
||||
The full decomp asset-extraction toolchain (`scripts/extract_baserom.u.sh`,
|
||||
`tools/extractor`, MIPS binutils) is **not** on the runtime path and never
|
||||
needs to reach an end user — it only regenerates the committed `assets/**`
|
||||
`.inc.c` / `.bin` metadata that a plain `git clone` already has.
|
||||
|
||||
### Important facts established while writing this plan
|
||||
|
||||
1. **`gen_romassets.py` does not need the ROM.** It resolves every symbol for
|
||||
`u`, `e`, and `j` from committed files (`scripts/filelist.<r>.csv`,
|
||||
`assets/obseg/file_resource_table.inc.c`, `assets/ramrom/ramrom.s`,
|
||||
`assets/music/*.s`, `ge007.ld`). The ROM is only opened to size the
|
||||
trailing `images` segment, and there is already a CSV-derived fallback when
|
||||
it is absent. So all three `romassets_<r>.s` files can be generated in CI
|
||||
with no ROM. (`docs/building.md` claims PAL/JP need "region-specific
|
||||
prop/font offsets from that region's ROM" — the current generator code
|
||||
shows no such ROM read; treat that sentence as stale, but verify before
|
||||
relying on it — see Open questions.)
|
||||
|
||||
2. **`d43_emit.py` / `d69_emit.py` are pure-stdlib Python 3.** They read only
|
||||
the ROM plus committed repo files, and their output is a deterministic
|
||||
function of the ROM. Both carry extensive built-in round-trip validation
|
||||
(re-parse the emitted image, check every field against the N64 source) and
|
||||
exit non-zero on any mismatch. `d43_emit.py` also runs a battery of
|
||||
buffer-fit cross-checks.
|
||||
|
||||
3. **The engine is region-locked at compile time.** `REGION_DEFS` in
|
||||
`CMakeLists.txt` differ per region (`VERSION_US/EU/JP`, `BUGFIX_*`,
|
||||
`LEFTOVERDEBUG`, …) and are used in 60+ game files. One exe cannot serve
|
||||
all three ROMs; the release must ship three exes (or one launcher that
|
||||
dispatches to three).
|
||||
|
||||
4. **Data/config location is already sane.** `sysResolvePath()` maps `$S/` to
|
||||
`./data/` (next to CWD if present, else next to the exe). `ge007.ini`,
|
||||
`ge007.eep`, and the sidecar trees all live under `$S/`. First launch
|
||||
already writes `ge007.ini` there. The sidecar loaders already search
|
||||
`$S/`, `$E/`, and `./`.
|
||||
|
||||
5. **ROM identity checking already half-exists.** `port/src/romdata.c`
|
||||
validates magic / title / cart-ID / country byte. `tools_pc/romverify.c`
|
||||
goes further (recomputes the `n64cksum` 6102 checksum over
|
||||
`[0x1000,0x100000)` using `tools/n64cksum.c`). Canonical SHA-1s for all
|
||||
three regions are in the README requirements table.
|
||||
|
||||
## Recommended approach
|
||||
|
||||
**Two-part: (A) fold the ROM-independent piece into CI now, and (B) add a
|
||||
native first-run converter to the engine so the sidecars are generated on the
|
||||
user's machine from their own ROM.**
|
||||
|
||||
### Part A — ship all three regions, pre-generate all `romassets`
|
||||
|
||||
Small, low-risk, unblocks a "download three exes, pick the one for your ROM"
|
||||
release even before Part B lands.
|
||||
|
||||
- CI `windows-build` becomes a 3-way matrix (`ntsc-final`, `pal-final`,
|
||||
`jpn-final`). Each leg runs `gen_romassets.py <u|e|j>` then `build-pc.sh`.
|
||||
- `bundle-win.sh` collects all three exes (`ge007.ntsc-final.exe`,
|
||||
`ge007.pal-final.exe`, `ge007.jpn-final.exe`) into one bundle, with shared
|
||||
DLLs.
|
||||
- Add a tiny **launcher** `ge007.exe` (≈150 lines C, its own CMake target):
|
||||
reads `data/*.z64` (and `baserom.*.z64`), checks the header country byte +
|
||||
SHA-1 against the three known-good hashes, then `CreateProcess`es the
|
||||
matching region exe. On no ROM / unknown ROM it shows a message box naming
|
||||
the accepted versions and hashes and the folder to drop the ROM in.
|
||||
- Alternative if a separate launcher is unwanted: make each region exe, on
|
||||
detecting a ROM whose country byte is for a different region, re-exec its
|
||||
sibling. Slightly hackier; avoids a fourth binary.
|
||||
|
||||
### Part B — native first-run sidecar converter
|
||||
|
||||
Port `d43_emit.py` and `d69_emit.py` to C as a new engine module
|
||||
`port/src/pcconvert/` (files: `pcconvert.h`, `d43.c`, `d69.c`,
|
||||
`pcconvert.c` orchestrator). On startup, `romdataInit()` (after it has the
|
||||
mapped ROM and region) calls `pcconvertEnsureSidecars(region)`:
|
||||
|
||||
1. If `$S/pcmodels-<region>/pcmodels.bin` **and** `$S/pccg-<region>/pccg.bin`
|
||||
exist and their embedded format-version tag matches the build → return
|
||||
immediately (the common case after first run).
|
||||
2. Otherwise show a "Preparing game data from your ROM (one-time, ~5 s)…"
|
||||
splash / log line, run the two converters, write the four output files
|
||||
into `$S/`, then continue boot. Cache a `pcconvert.stamp` with the ROM
|
||||
SHA-1 + converter version so a ROM swap or engine upgrade re-runs it.
|
||||
|
||||
The converters need, besides the ROM:
|
||||
|
||||
- `scripts/filelist.u.csv`
|
||||
- `assets/obseg/file_resource_table.inc.c`
|
||||
- `assets/**/ModelFileHeader.inc.c` (NS/NT per model)
|
||||
- the stage `bg` / `stan` `.inc.c` metadata `d69` reads
|
||||
|
||||
These are committed text/CSV, a few hundred KB total. **Embed them into the
|
||||
exe at build time** via a CMake codegen step (`file(READ ...)` →
|
||||
`.byte`/`const char[]`), so the release stays "exe + DLLs" with nothing loose.
|
||||
A build-file change is required here (new `add_custom_command` + generated
|
||||
source) — out of scope for this doc, but noted as the one unavoidable
|
||||
`CMakeLists.txt` edit.
|
||||
|
||||
Reuse: `zlib` is already linked (RZ = `0x11 0x72` + raw deflate — the exact
|
||||
codec `pcmodels`/`pccg` already decompress). The engine already has robust
|
||||
byte-swap helpers in `romdata.c`. The Python round-trip validators should be
|
||||
ported too and run in debug builds / behind an env flag.
|
||||
|
||||
**Effort estimate:** `d43_emit.py` is ~1000 lines of intricate model-graph
|
||||
simulation (LOD/SWITCH rewire, BSP splice, marker-expansion bounds, DFS
|
||||
placement) + validation; `d69_emit.py` ~500 lines. Call it a 2–4 week
|
||||
focused task with careful diffing of C output against the Python output
|
||||
byte-for-byte on all three ROMs before trust. This is the single biggest
|
||||
work item.
|
||||
|
||||
### Interim option (faster Part B, if timeline pressure)
|
||||
|
||||
Freeze the two emit scripts with **PyInstaller / Nuitka in the Windows CI
|
||||
job** (the runner has Python) into one `ge007-convert.exe`, ship it in the
|
||||
bundle, and have the engine `CreateProcess` it on first run when sidecars are
|
||||
missing. Pure-stdlib scripts freeze into a single clean exe with no user
|
||||
Python. Downsides: ~8–15 MB added to the bundle, Windows-only, and it is a
|
||||
second binary to sign / trust. Recommend this **only** as a stopgap that buys
|
||||
time for the real C port; it satisfies "no user-visible Python/MSYS2" but not
|
||||
"single native binary" or non-Windows.
|
||||
|
||||
### Rejected options
|
||||
|
||||
- **Precompute and ship the sidecars.** They are ROM-derived copyrighted game
|
||||
data (`data/pcmodels-*`, `data/pccg-*` are gitignored precisely for this).
|
||||
Redistribution is not acceptable. Rejected.
|
||||
- **Region-independent sidecars.** `d43`/`d69` output is a function of the
|
||||
ROM; there is no region-independent form to precompute, and even if there
|
||||
were it would still be ROM-derived data. Rejected.
|
||||
- **Bundle CPython + the .py scripts loose.** Meets the letter of "no MSYS2"
|
||||
but not "no Python", ages badly, and is bigger than a frozen exe for no
|
||||
gain. Rejected in favour of the interim freeze if a stopgap is needed.
|
||||
|
||||
## CI / release changes
|
||||
|
||||
1. `windows-build` → matrix over the three ROMIDs; each leg generates its
|
||||
`romassets_<r>.s` and builds.
|
||||
2. Commit all three `port/src/romassets_{u,e,j}.s` (or generate in each CI
|
||||
leg — committing is simpler and lets non-CI builds skip the generator).
|
||||
Add an `e`/`j` regen + `git diff --stat` check to the `validate` job
|
||||
mirroring the existing US one.
|
||||
3. `bundle-win.sh`: bundle all three region exes + the launcher; keep the
|
||||
"no ROM / size ≤ 60 MB" guards (raise the limit only if the interim frozen
|
||||
converter is included).
|
||||
4. `release` job: unchanged shape (draft pre-release); update
|
||||
`.github/release-notes.md` with the drop-in-ROM instructions + the
|
||||
requirements/hash table.
|
||||
5. Add a CI check that runs `d43_emit.py --check-only` / a `d69` equivalent
|
||||
against a **CI-only** ROM if one can be provided via encrypted secret;
|
||||
otherwise this stays a manual pre-publish smoke test (the release job is
|
||||
already a human-gated draft).
|
||||
6. Once Part B lands: a CI step that builds the C converter, runs it and the
|
||||
Python converter on the same ROM (secret), and `cmp`s the outputs.
|
||||
|
||||
## Code to be written
|
||||
|
||||
| File(s) | Rough scope | Notes |
|
||||
|---|---|---|
|
||||
| `port/src/launcher/ge007_launch.c` + CMake target | ~150 lines | ROM detect, SHA-1 match vs 3 known hashes, `CreateProcess` region exe, message-box on failure |
|
||||
| `port/src/pcconvert/d43.c` | ~1000–1300 lines | C port of `tools_pc/d43_emit.py` incl. graph sim + validation |
|
||||
| `port/src/pcconvert/d69.c` | ~500–700 lines | C port of `tools_pc/d69_emit.py` |
|
||||
| `port/src/pcconvert/pcconvert.c` + `.h` | ~200 lines | orchestrator, stamp file, splash text, wired into `romdataInit()` |
|
||||
| CMake codegen for embedded metadata | ~40 lines CMake + generated `.c` | embeds `filelist.u.csv`, `file_resource_table.inc.c`, `ModelFileHeader.inc.c`, bg/stan `.inc.c` |
|
||||
| `tools_pc/bundle-win.sh`, `.github/workflows/ci.yml` | edits | matrix, 3 exes, launcher, notes |
|
||||
| SHA-1 helper (or reuse a vendored small SHA-1) | ~100 lines | for launcher + `pcconvert.stamp`; `romverify.c`'s n64cksum path is an alternative identity check |
|
||||
|
||||
## ROM SHA-1 / region handling
|
||||
|
||||
- Accept a ROM at `data/ge007.<region>.z64`, `data/*.z64` (any name — sniff
|
||||
the header), or `baserom.{u,e,j}.z64`.
|
||||
- Verify: N64 magic + "GOLDENEYE" + "GE" + country byte (already done), then
|
||||
SHA-1 against the README table. On a header-valid but hash-mismatched ROM
|
||||
(bad dump, byte-swapped `.n64`/`.v64`, ROM hack): warn but allow a
|
||||
`--skip-rom-check` / ini opt-out; byte-swapped images should be detected and
|
||||
the user told to supply a true big-endian `.z64`.
|
||||
- Region is taken from the country byte (`E`→ntsc-final, `P`→pal-final,
|
||||
`J`→jpn-final), which already drives `pcmodelsRegionForCountry()`.
|
||||
|
||||
## Config / data location
|
||||
|
||||
No change needed. `$S/` = `./data/` (or `<exedir>/data/`). First run creates
|
||||
`ge007.ini`; Part B additionally creates `pcmodels-<region>/`,
|
||||
`pccg-<region>/`, and `pcconvert.stamp` there. Document that the user makes a
|
||||
`data/` folder next to the exe and drops the ROM in it (the launcher can
|
||||
offer to create it).
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Does PAL/JP `romassets` generation truly need nothing from the ROM?**
|
||||
`docs/building.md` says it needs region-specific offsets; the code says
|
||||
otherwise. Generate `e` and `j` in CI and diff against a
|
||||
locally-ROM-generated copy to confirm before committing them.
|
||||
2. **Does `d43_emit.py` really use `scripts/filelist.u.csv` for all three
|
||||
regions?** It hard-codes `FILELIST = "scripts/filelist.u.csv"` even when
|
||||
`REGION` is `pal-final`/`jpn-final`. Either model file offsets are
|
||||
US-identical across regions (plausible — models live in obseg which may not
|
||||
shift) or PAL/JP model conversion is currently untested. Confirm before
|
||||
promising PAL/JP support in the release.
|
||||
3. **Launcher vs self-re-exec vs three shortcuts** — pick one UX.
|
||||
4. **Splash UI for the first-run convert** — reuse the SDL window / a simple
|
||||
progress line, or a pre-SDL Win32 dialog? ~5 s is short enough that a log
|
||||
line + busy cursor may suffice.
|
||||
5. **Non-Windows.** Part B's C converter is portable; the launcher and
|
||||
bundling are Windows-only today. Out of scope but the C port keeps the
|
||||
door open.
|
||||
6. **Signing.** Unsigned exes + a "reads your ROM" first-run step will trip
|
||||
SmartScreen. Not blocking, but worth a release-notes note.
|
||||
|
||||
## Suggested milestone ordering
|
||||
|
||||
1. Part A (3-region matrix + pre-generated `romassets` + launcher) — days.
|
||||
2. Interim frozen `ge007-convert.exe` wired to first-run — days, optional.
|
||||
3. Part B C port of `d43`/`d69` + embedded metadata + output diffing — weeks.
|
||||
4. Drop the interim binary; single-binary drop-in experience complete.
|
||||
+1
-1
@@ -13,7 +13,7 @@ graphics coprocessor in software. It follows the architecture of the
|
||||
|
||||
It is also a **research project on AI-agent collaboration in a large,
|
||||
unfamiliar, low-level codebase**: two coding agents — a local open-weight model
|
||||
(Qwen 3) on a single RTX 5090, and Claude / Claude Code — driven by one person
|
||||
(Qwen 3.8) on a single RTX 5090, and Claude / Claude Code — driven by one person
|
||||
part-time through ~230 translation units of unmodified big-endian MIPS game
|
||||
code, handing work back and forth through shared written notes.
|
||||
|
||||
|
||||
Reference in new issue
Block a user