forked from Ports/goldeneye-pc-port
Prepare for public release
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING. - Restructure docs: internals.md, porting-notes.md, dev-process.md + dev/agentic-development.md (the research write-up), dev/ (raw record). - Replace decomp status-badge CI with a PC-port validate workflow. - Remove session/agent scaffolding and stale planning docs from the tree. - Add .gitattributes (normalize line endings to LF); add screenshots. The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken (KholdFuzion) and contributors, preserved unchanged. The PC-port work from 2026-08-17 on is mine.
This commit is contained in:
1 parent
294e24cc26
commit
52539d10de
118 files changed
+3995
-8319
No files matched your search
@@ -0,0 +1,357 @@
|
||||
# Audio Plan — Phase 3 (libaudio → SDL)
|
||||
|
||||
Status: **draft for evaluation** (written read-only; no code changes made).
|
||||
Supersedes the audio paragraphs of §6 in `docs/internals.md` as the plan of record
|
||||
for closing **D77** (no audible music/sound on PC). Companion docs: `docs/HANDOFF.md`,
|
||||
`docs/porting-notes.md`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Goal and scope
|
||||
|
||||
Make GoldenEye's audio audible on the PC port with 1:1 fidelity to the N64 pipeline:
|
||||
music (3 cseq tracks) and SFX through the real libaudio synth, out of SDL2.
|
||||
|
||||
In scope:
|
||||
- Executing the per-frame **Acmd stream** that stock IDO libaudio emits (the part that
|
||||
ran on the RSP under GE's `aspMain` microcode).
|
||||
- SDL audio device + queue plumbing (`port/src/audio.c`, AI shims in `libultra.c`).
|
||||
- One latent ABI fix in the port shim headers (`K0_TO_PHYS`, §7.1).
|
||||
|
||||
Out of scope:
|
||||
- MP3 playback (naudio-only, PD-specific; GE never emits it).
|
||||
- Input (done — D118), saves (Phase 4).
|
||||
- Any change to game logic. All work lives in `port/` except the narrow, documented
|
||||
shim-header exception in §7.1 (same class as D78).
|
||||
|
||||
## 2. Current state (verified by reading, not assumption)
|
||||
|
||||
Working already:
|
||||
- **The whole CPU side of libaudio runs.** `src/libultra/audio/*` +
|
||||
`src/libultrare/audio/{drvrNew,env,reverb}.c` compile and execute; the audio thread
|
||||
(`amMain`, `src/audi.c`) gets retrace messages from the real scheduler (`src/sched.c`,
|
||||
compiled — Phase 2 decision), calls `alAudioFrame()`, which fills a 3000-word Acmd
|
||||
list and posts an `M_AUDTASK` to the scheduler. Bank trees, cseq fixups (D54, D36/D37)
|
||||
are done. Sample DMA from cart works via `amDmaCallback` → `osPiStartDma`
|
||||
(synchronous ROM-image shim in `libultra.c`).
|
||||
|
||||
Dead (the three gaps):
|
||||
1. **Acmd list never executed.** `port/src/libultra.c:1108` `osSpTaskStartGo()` has an
|
||||
explicit Phase-3 no-op branch for `M_AUDTASK`. The output buffer (`info->data`) is
|
||||
never filled.
|
||||
2. **AI shims are stubs.** `libultra.c:794–810`: `osAiSetNextBuffer` discards the
|
||||
buffer, `osAiGetLength` returns 0 (so the per-frame sample-count feedback loop in
|
||||
`amHandleFrameMessage`, `audi.c:527`, runs blind).
|
||||
3. **No SDL device.** `port/src/audio.c` `audioInit()` is a TODO stub; and nothing
|
||||
calls `audioEndFrame()` (zero callers repo-wide).
|
||||
|
||||
Existing PORT guards to revisit when audio lands:
|
||||
- **C7 / D127** (`src/snd.c:859`): skips SFX whose resolved `ALSound*` pointer is out of
|
||||
range — added because the converted bank tree (`romdata.c afFixupInst`) yields fewer/
|
||||
rearranged `soundArray` slots than the N64 image on some levels (Surface1). May be
|
||||
masking a real bank-tree defect. See **Q1**.
|
||||
- `src/audi.c:365`: `#if defined(PORT)` memcpy of `CUSTOM_FX_PARAMS_N` (C++-style
|
||||
initializer workaround) — harmless, keep.
|
||||
|
||||
## 3. The GE pipeline in one paragraph
|
||||
|
||||
Audio thread on each retrace: `amHandleFrameMessage()` (`src/audi.c:497`) hands the
|
||||
*previous* frame's output to AI via `osAiSetNextBuffer(lastInfo->data, …)`, computes
|
||||
this frame's sample count from `g_FrameSize - osAiGetLength()>>2` (clamped to
|
||||
`g_MinFrameSize`), calls `alAudioFrame()` which walks the filter chains (decoder →
|
||||
resampler → envmixer → [FX: GE's custom chorus/reverb] → aux bus → main bus → save)
|
||||
emitting **Acmd words** into a 3000-word list, then posts an `M_AUDTASK` (ucode =
|
||||
`aspMain`). The real scheduler (`sched.c:439,485`) queues it in the audio list and runs
|
||||
it through `osSpTaskLoad/osSpTaskStartGo`. On the N64 the RSP executes the Acmd stream
|
||||
against 4 KB of DMEM, writing interleaved s16 stereo to `info->data`; AI DMA then plays
|
||||
it at 22050 Hz. On PC we must replace "RSP executes Acmd stream" — that is the entire
|
||||
problem.
|
||||
|
||||
**Opcode inventory** (grep-verified across `src/libultra/audio/*.c` +
|
||||
`src/libultrare/audio/*.c` — 15 ops):
|
||||
|
||||
| Op | Emitted by | PD reference code? |
|
||||
|---|---|---|
|
||||
| `aSetBuffer` (×20) | load, mainbus, resample, save, reverb | yes (`a*Impl` pattern; different arg ABI) |
|
||||
| `aMix` (×7) | mainbus, reverb | yes — `aMixImpl` |
|
||||
| `aLoadBuffer` (×7) | load, reverb | yes — `aLoadBufferImpl` |
|
||||
| `aClearBuffer` (×7) | auxbus, mainbus, load, reverb | yes — `aClearBufferImpl` |
|
||||
| `aSetVolume` (×5) | env.c (Rare envelope) | yes — `aSetVolumeImpl` |
|
||||
| `aSaveBuffer` (×4) | save, reverb | yes — `aSaveBufferImpl` |
|
||||
| `aDMEMMove` (×4) | load, resample, reverb | yes — `aDMEMMoveImpl` |
|
||||
| `aResample` (×2) | resample.c, reverb.c | yes — `aResampleImpl` (SSE4.1/NEON/scalar) |
|
||||
| `aLoadADPCM` (×2) | load.c, reverb.c | yes — `aLoadADPCMImpl` |
|
||||
| `aSetLoop` | load.c:479 | yes — `aSetLoopImpl` |
|
||||
| `aSegment` | synthesizer.c:231 | n/a (no-op on CPU) |
|
||||
| `aPoleFilter` | **reverb.c:402 (GE custom FX)** | **NO — PD's stub is empty ("never gets called?")** |
|
||||
| `aInterleave` | save.c | yes — `aInterleaveImpl` (naudio variant takes no args) |
|
||||
| `aEnvMixer` | env.c:410/414 | yes — `aEnvMixerImpl` |
|
||||
| `aADPCMdec` | load.c:483 | yes — `aADPCMdecImpl` (SSE4.1/NEON/scalar) |
|
||||
|
||||
PD reference = the Perfect Dark port `port/src/mixer.c` +
|
||||
`port/include/mixer.h`. Reusable: the DSP *math* (ADPCM 8-coefficient loop filter,
|
||||
resample table + interpolation, envelope ramping, clamps). **Not** reusable as-is:
|
||||
signatures and argument conventions (naudio ABI ≠ IDO ABI, §5.3).
|
||||
|
||||
## 4. How the PD port does it (ground-truth comparison)
|
||||
|
||||
1. naudio is Rare's own library; its `n_abi.h` has a `#ifndef PLATFORM_N64` branch that
|
||||
redefines every `n_a*` macro as a **direct C call** into `port/src/mixer.c`
|
||||
(`mixer.h` `#undef`s the macros and redirects to `a*Impl`). All DSP therefore runs
|
||||
**inline on the audio thread at task-build time**, inside `amgrFrame()`.
|
||||
2. The `M_AUDTASK` is an empty shell: `pdsched.c:233` receives it with the comment
|
||||
`// send this into the mixer` and an empty body.
|
||||
3. PD **replaced the scheduler** (`pdsched.c`) and drives audio from retrace:
|
||||
`schedAudioFrame()` → `amgrFrame(); audioEndFrame();` (×`diffframe60`). Same thread
|
||||
for build and flush ⇒ no locking in their SDL handoff.
|
||||
4. SDL device layer: queue-based, exactly what our scaffold already copied —
|
||||
`osAiGetLength` = queued **bytes** (`audioGetBytesBuffered()`),
|
||||
`osAiSetNextBuffer` = stash pointer, `audioEndFrame` = `SDL_QueueAudio` with a
|
||||
`queueLimit` backpressure drop. Device opened at 22020 Hz (their TODO admits this
|
||||
"might cause trouble" — it is a silent −9-cent detune vs their 22050 synth rate).
|
||||
5. DMEM: ~3 KB static buffer, **naudio-specific** offsets (main L/R @0x4E0/0x650,
|
||||
aux @0x7C0/0x930) — different from IDO's layout (§6).
|
||||
6. PD-only extras: `aPlayMP3Impl` (minimp3; GE never emits), `aPoleFilterImpl` empty
|
||||
stub (GE *does* emit it).
|
||||
|
||||
**Why we can't copy it wholesale:** GE compiles the real `src/sched.c` and stock IDO
|
||||
libaudio, whose macros only *write words* into the Acmd list — there is no build-time
|
||||
hook unless we shadow `PR/abi.h`. Something must execute the stream. Two further ABI
|
||||
facts:
|
||||
|
||||
- **IDO's implicit "current buffer":** e.g. `aADPCMdec(pkt, f, s)` carries only flags +
|
||||
a state pointer; in/out DMEM offsets and sample count come from the preceding
|
||||
`aSetBuffer` (`load.c:482–483`, `resample.c:90–91`). naudio spells everything out per
|
||||
call. Whatever option we pick, the implementation must model current-buffer state.
|
||||
- **Addresses in the stream are RAM pointers** (`K0_TO_PHYS(f->state)`,
|
||||
`osVirtualToPhysical(e->state)`, `f->dramout`) — resolvable on PC *except* for the
|
||||
`K0_TO_PHYS` bug in §7.1.
|
||||
|
||||
## 5. Architecture options (evaluate all; recommendation marked)
|
||||
|
||||
### Option A — CPU-side Acmd interpreter at task time 【RECOMMENDED】
|
||||
|
||||
Decode the 64-bit command words from `t->t.data_ptr` inside the existing `M_AUDTASK`
|
||||
branch of `osSpTaskStartGo()` (`libultra.c:1108`), dispatching each op to an
|
||||
implementation in `port/src/mixer.c` operating on a static 4 KB "DMEM".
|
||||
|
||||
- Pros: zero game/header/build changes; treats the stream as data exactly like fast3d
|
||||
does with GBI (project-consistent); natural logging hook (`GE_DAUDIO=1` dump of op +
|
||||
args per frame) for debugging and for validating against the microcode disassembly;
|
||||
execution point matches N64 timing (DSP runs when the RSP would have run it).
|
||||
- Cons: must write the word decoder (~50 lines, trivial — op is bits 24–31 of w0 per
|
||||
`include/PR/abi.h`) and the current-buffer state machine; one scheduler tick of extra
|
||||
latency vs build-time execution (inaudible).
|
||||
|
||||
### Option B — Shadow `PR/abi.h`, redirect macros to direct C calls (the PD trick)
|
||||
|
||||
Add `port/shim/PR/abi.h`: `#include "include/PR/abi.h"` then `#undef` the 15 `a*`
|
||||
macros and `#define` them as direct calls into `mixer.c` (PD's `mixer.h` does exactly
|
||||
this for naudio). DSP then runs inline during `alAudioFrame()` on the audio thread; the
|
||||
`M_AUDTASK` branch stays a no-op, exactly like PD.
|
||||
|
||||
- Pros: battle-tested pattern (shipped in PD); no decoder; slightly less latency.
|
||||
- Cons: shadows a core PR header for every TU that includes it (only libaudio uses these
|
||||
macros on PC, so blast radius is small but nonzero — must verify include-path
|
||||
ordering in `CMakeLists.txt`); stream becomes invisible at runtime (no logging hook —
|
||||
you'd instrument inside each Impl); the shim must exactly mirror IDO arg order, which
|
||||
differs from PD's naudio redirects line-by-line.
|
||||
|
||||
### Option C — Run the real `aspMain` microcode 【NOT RECOMMENDED】
|
||||
|
||||
Point `aspMainTextStart/DataStart` (`port/src/ucode.c`) at the ROM microcode and execute
|
||||
it with an RSP emulator. Maximum fidelity by construction, but requires a 16-bit-MIPS
|
||||
RSP core + DMEM + interrupt model for one component whose entire job is the 15 ops we
|
||||
can implement directly. Keep the `ucode.c` dummies permanently (their header comment
|
||||
anticipates this decision). The microcode is still *used* — as a **reference to
|
||||
disassemble**, not to run (§8, Phase 0).
|
||||
|
||||
### Sub-decision A1 — where samples hit the SDL queue 【RECOMMENDED: immediate】
|
||||
|
||||
- **Immediate:** `osAiSetNextBuffer()` shim calls into `audio.c` and `SDL_QueueAudio`s
|
||||
right there (on the audio thread), with the existing `queueLimit` backpressure drop.
|
||||
Lock-free (single writer); matches the moment N64 AI DMA starts; `audioEndFrame()`
|
||||
disappears entirely. Safe because `NUMBER_OUTPUT_BUFFERS=3` rotation guarantees the
|
||||
handed-off buffer isn't rewritten before it is queued.
|
||||
- **Deferred (PD-style):** stash in `osAiSetNextBuffer`, flush from a frame-end hook.
|
||||
In GE this crosses threads (audio thread sets; scheduler/gfx path would flush) ⇒ needs
|
||||
a mutex or accepts PD's unlocked pattern, and needs a new `audioEndFrame()` call site
|
||||
that doesn't exist today. Only worth it if immediate queueing shows a problem.
|
||||
|
||||
### Sub-decision A2 — device sample rate 【RECOMMENDED: 22050】
|
||||
|
||||
Request exactly `OUTPUT_RATE` (0x5622 = 22050 Hz), stereo s16, as PD's `audio.c`
|
||||
scaffold already comments. Do **not** copy PD's shipped 22020 (detune). If a platform
|
||||
rejects 22050, let SDL convert (`SDL_OpenAudioDevice` with allowed-conversion) rather
|
||||
than changing the requested rate.
|
||||
|
||||
### Sub-decision A3 — timing and thread priorities 【RECOMMENDED: inline + host priorities; no worker thread】
|
||||
|
||||
Execution stays on the scheduler thread (Option A's execution point). If Phase 2/3
|
||||
milestones show underruns (`audio: ai out of samples` spam — verification step 3) or
|
||||
frame-time spikes attributable to synthesis cost, the **first lever is host thread
|
||||
priorities**: `SetThreadPriority` / `pthread_attr_setschedparam` with the scheduler
|
||||
thread time-critical (it now owns both fast3d and the mixer) and the tick thread
|
||||
normal — a ~10-line fix in `libultra.c`. A dedicated audio worker thread consuming
|
||||
`M_AUDTASK`s is explicitly **not** the next step: it adds a cross-thread handoff for a
|
||||
problem priorities may fully solve, and the kernel's priority semantics are already
|
||||
fake (D24 — see the backlogged bug-class entry in `docs/BACKLOG.md`), so any deeper
|
||||
redesign starts there. Do **not** consider rewriting toward PD's single-threaded
|
||||
inline-`amgrFrame()` model: it works for them only because they adapted game-side lib
|
||||
code (direct `schedSubmitTask` calls, gutted `pdsched.c`); for GE it means editing
|
||||
`src/audi.c` / scheduler submission points — a non-negotiable #2 violation. Rationale
|
||||
recorded in `docs/BACKLOG.md` (Architecture decision).
|
||||
|
||||
## 6. DMEM layout (IDO, from `src/libultra/audio/synthInternals.h:57–71`)
|
||||
|
||||
```
|
||||
AL_MAX_RSP_SAMPLES = 160 (per-chunk; alAudioFrame builds the list in ≤160-sample chunks)
|
||||
offset 0 AL_DECODER_IN / AL_RESAMPLER_OUT / AL_TEMP_0 (320 B)
|
||||
offset 320 AL_DECODER_OUT / AL_TEMP_1 (320 B)
|
||||
offset 640 AL_TEMP_2 (320 B)
|
||||
offset 1088 AL_MAIN_L_OUT (320 B)
|
||||
offset 1408 AL_MAIN_R_OUT (320 B)
|
||||
offset 1728 AL_AUX_L_OUT (320 B)
|
||||
offset 2048 AL_AUX_R_OUT (320 B) → max extent 2368 B
|
||||
```
|
||||
|
||||
Static 4 KB buffer in `mixer.c` (round number, headroom). **Not** PD's naudio offsets.
|
||||
GE's custom FX (`reverb.c`) uses `AL_TEMP_0/1/2` as scratch plus RAM-backed delay lines
|
||||
via `aLoadBuffer`/`aSaveBuffer` to `r->base` — all plain host pointers on PC.
|
||||
|
||||
## 7. Shims and ABI fixes required
|
||||
|
||||
### 7.1 【BUG FOUND】 `K0_TO_PHYS` is unshimmed and corrupts pointers on PC
|
||||
|
||||
`port/shim/PR/R4300.h` redefines `PHYS_TO_K0` (identity) but **not** `K0_TO_PHYS`.
|
||||
Stock: `#define K0_TO_PHYS(x) ((x)&0x1FFFFFFF)` (`include/PR/R4300.h:65`). Game RAM
|
||||
lives at the V1 view `0x70xxxxxx` (`port/src/dram.c`); masking gives `0x10xxxxxx` — an
|
||||
invalid host pointer. Hit sites: `load.c:483` (`aADPCMdec(..., K0_TO_PHYS(f->state))`)
|
||||
and `load.c:479` (`aSetLoop(ptr++, K0_TO_PHYS(f->lstate))`) — the mixer would receive
|
||||
garbage state addresses for every ADPCM voice. Fix: extend the existing shim with
|
||||
`#undef K0_TO_PHYS / #define K0_TO_PHYS(x) ((u32)(x))` (identity; V1 addresses are live
|
||||
host pointers). Inert in the N64 build (no `-DPORT`) — same class as the D78 precedent.
|
||||
Record as a new Dxx finding when implemented (next free label after D127).
|
||||
|
||||
### 7.2 AI shims (`libultra.c:794–810`)
|
||||
|
||||
- `osAiSetFrequency(hz)` → return `hz` (already does; 22050 flows into
|
||||
`alconf->outputRate` and the frame-size math — correct).
|
||||
- `osAiSetNextBuffer(buf, size_bytes)` → queue to SDL per A1.
|
||||
- `osAiGetLength()` → **bytes** currently queued (`SDL_GetQueuedAudioSize`); the game
|
||||
does `>> 2` for frames (`audi.c:527`). PD returns bytes too. This closes the
|
||||
feedback loop that sizes each frame's synthesis.
|
||||
- `osAiSetConvert` — not called by GE; leave stub.
|
||||
|
||||
### 7.3 Misc
|
||||
|
||||
- `port/src/audio.c`: fill in `audioInit()` (SDL_InitSubSystem(AUDIO), open device per
|
||||
A2, unpause); add the queue function per A1; keep `Audio.BufferSize`/`Audio.QueueLimit`
|
||||
config entries; add an `Audio.Enabled` toggle (PD's `g_SndDisabled`) so the level
|
||||
sweep can run silent.
|
||||
- `port/src/mixer.c`: rewrite as the op implementations + (for Option A) the decoder /
|
||||
current-buffer state machine. `mixerInit/Destroy` reset DMEM + op state.
|
||||
- Threading note: mixer runs on the scheduler thread (`sched.c __scMain` →
|
||||
`osSpTaskStartGo` inline); with A1 there is exactly one shared object (the SDL queue,
|
||||
internally locked) — no port-side locks needed.
|
||||
|
||||
## 8. Phase 0 — recon (do first; informs everything)
|
||||
|
||||
1. **Disassemble `bin/aspboot.text.bin`** (+ `.data.bin`; markers in `src/aspboot.s`) —
|
||||
GE's real ASP microcode, 16-bit MIPS RSP. This is the ground truth for:
|
||||
- exact `aSetBuffer` current-buffer semantics (which subsequent ops consume it, and
|
||||
how),
|
||||
- `aADPCMdec` state layout (what the pointer in w1 points at — IDO `ADPCM_STATE` +
|
||||
data addressing),
|
||||
- **`aPoleFilter`** — the one op with no PD reference code; implement from this,
|
||||
- any DMEM convention not derivable from libaudio usage.
|
||||
Methodology mirrors how `rsp/graphics/gmain.s` is used for graphics. A small
|
||||
16-bit-MIPS disassembler script (`tools_pc/`) is sufficient — we decode, not execute.
|
||||
2. **Acmd stream logger** (`GE_DAUDIO=1`, in the Option-A decoder or a temporary probe
|
||||
in the `M_AUDTASK` branch): dump op + decoded args per frame for the first N frames.
|
||||
Confirms the 15-op inventory at runtime, catches surprises (e.g. ops we didn't grep),
|
||||
and gives before/after artifacts for validating the implementation.
|
||||
|
||||
## 9. Open questions (decide before Phase 1)
|
||||
|
||||
- **Q1 — C7/D127 bank-tree guard (`src/snd.c:859`):** root-cause the bogus
|
||||
`ALSound*` resolutions now (likely an `afFixupInst` layout/stride defect in
|
||||
`port/src/romdata.c`, same family as D37's bank-tree re-layout), or keep the guard as
|
||||
a documented stopgap and file a separate finding? Keeping it means some SFX stay
|
||||
silently missing on affected levels (Surface1) even after audio lands.
|
||||
- **Q2 — Option A vs B** (§5): A is recommended (no header shadowing, stream logging,
|
||||
project-consistent with fast3d); B is the PD-proven fallback if current-buffer
|
||||
semantics prove fiddly at task time. Note for the evaluator: this is a **low-stakes,
|
||||
cheap-to-revisit decision** — both options share 100% of the DSP implementation code
|
||||
(§6 layout, all op bodies); only the ~50-line front end (word decoder vs macro
|
||||
redirects) and the execution point differ. If Phase 0's disassembly surfaces anything
|
||||
that makes B look safer, switching costs an afternoon, not a re-architecture.
|
||||
- **Q3 — Pole-filter fidelity bar:** implement `aPoleFilter` strictly from the
|
||||
microcode disassembly (recommended), or accept a devkit-docs approximation and verify
|
||||
by ear? The chorus is audible in most music; a wrong filter coefficient shows as tonal
|
||||
difference, not crash.
|
||||
|
||||
## 10. Phases and milestones
|
||||
|
||||
- **Phase 0 — Recon** (§8): microcode disassembly notes + Acmd log artifacts committed
|
||||
to `docs/` (or `tools_pc/`). Exit: current-buffer semantics + pole filter documented;
|
||||
op inventory confirmed at runtime.
|
||||
- **Phase 1 — Plumbing.** SDL device opens @22050 stereo s16 (A2); A1 queue path live;
|
||||
real AI shims (§7.2); `K0_TO_PHYS` shim fix (§7.1); `Audio.Enabled` toggle. Mixer still
|
||||
a no-op is fine. **Milestone: device drains, `osAiGetLength` feedback loop visible in
|
||||
logs, clean start/stop, silence (not crash).**
|
||||
- **Phase 2 — Core ops.** Implement `aSetBuffer/aLoadBuffer/aClearBuffer/aDMEMMove/
|
||||
aADPCMdec/aResample/aEnvMixer/aMix/aSetVolume/aLoadADPCM/aSetLoop/aInterleave/
|
||||
aSaveBuffer` (+`aSegment` no-op) in `mixer.c`, reusing PD's DSP math where the ABI
|
||||
maps. **Milestone: SFX audible (gunshots, doors); dry music plays.** Expect at least
|
||||
one Dxx-class finding here (state-layout or endianness surprise is the usual bug
|
||||
class — see `docs/porting-notes.md`).
|
||||
- **Phase 3 — FX.** `aPoleFilter` + validate the full `reverb.c` chorus path against
|
||||
the disassembly. **Milestone: music matches N64 character (chorus/reverb present, no
|
||||
pumping); close D77.**
|
||||
- **Phase 4 — Polish & bookkeeping.** Resolve Q1; remove/adjust temporary probes and
|
||||
`GE_DAUDIO` defaults; latency/buffer tuning (`Audio.BufferSize`, `QueueLimit`);
|
||||
host thread-priority tuning per A3 *only if* underruns were observed in Phases 2–3;
|
||||
record findings in `docs/internals.md` §F/§H (next Dxx after D127) + update the
|
||||
§F index, `AGENTS.md` Phase 3 status, `docs/HANDOFF.md`; append recurring bug classes
|
||||
to `docs/porting-notes.md`.
|
||||
|
||||
## 11. Verification ritual (audio-specific, on top of the standard one in AGENTS.md)
|
||||
|
||||
1. Boot → title: menu music audible within a couple of seconds; no click at start.
|
||||
2. Enter BUNKER1 (`-level_09`): gunfire/door SFX present and roughly level-correct;
|
||||
level music plays.
|
||||
3. No `audio: ai out of samples` spam from `amHandleDoneMessage` (`audi.c:596`) — that
|
||||
print is the built-in underrun detector.
|
||||
4. Run 2–3 levels incl. one with heavy SFX overlap; watch `SDL_GetQueuedAudioSize`
|
||||
(log it) staying below `queueLimit` in steady state.
|
||||
5. PAL build (`pal-final`) spot-check: 50 Hz retrace drives the same path; frame-size
|
||||
math uses `MAYBE_FRAME_RATE` — confirm no underruns there too.
|
||||
6. `Audio.Enabled=0` → fully silent, zero audio CPU cost, nothing else affected.
|
||||
|
||||
## 12. Risks / unknowns
|
||||
|
||||
| Risk | Mitigation |
|
||||
|---|---|
|
||||
| IDO current-buffer semantics misread → subtle wrong output (no crash) | Phase 0 disassembly is the arbiter; Acmd logger + by-ear checks per phase |
|
||||
| `aPoleFilter` has no PD code | Disassembly (Q3); it's one small IIR — bounded effort |
|
||||
| ADPCM state layout differs from assumption → garbage/cratchy samples | Phase 0 documents the struct from `load.c` usage + microcode; scalar fallback path makes debugging easy |
|
||||
| C7/D127 hides a bank-tree bug that audio makes *audible* (wrong SFX, not just missing) | Q1; keep the guard until root-caused |
|
||||
| SDL queue backpressure drops under load (slow machines) | `queueLimit` drop is graceful (silence gap, no crash); A3: raise scheduler-thread host priority before buffer tuning; PD shipped the same design |
|
||||
| Synthesis cost on the scheduler thread starves gfx frame time / causes underruns | A3: host thread priorities first; a dedicated audio worker thread only if priorities provably insufficient (and revisit D24's fake-priority semantics before designing one) |
|
||||
| Cross-region: PAL sample-count math differs | Verification step 5 |
|
||||
|
||||
## 13. Key references
|
||||
|
||||
| File | What it gives us |
|
||||
|---|---|
|
||||
| `src/audi.c` | audio thread, frame-size feedback loop, task construction (`OUTPUT_RATE` @22) |
|
||||
| `src/sched.c:439,465,485` | real scheduler's M_AUDTASK path → `osSpTaskStartGo` interception point |
|
||||
| `include/PR/abi.h:260–407` | IDO Acmd word formats (the decoder's spec) + A_* opcodes |
|
||||
| `src/libultra/audio/synthInternals.h:57–71` | DMEM layout constants |
|
||||
| `src/libultrare/audio/reverb.c`, `env.c`, `drvrNew.c` | GE custom FX (`alFxNew`), pole filter + chorus, envelope ops |
|
||||
| `bin/aspboot.text.bin` / `.data.bin` (+ `src/aspboot.s`) | **ground-truth ASP microcode to disassemble** |
|
||||
| `pd_port/port/src/mixer.c`, `port/include/mixer.h` | DSP math reference + the Option-B pattern |
|
||||
| `pd_port/port/src/audio.c`, `pdsched.c:233–260`, `libultra.c:150–175` | PD wiring (queue design, empty AUDTASK branch, AI shims) |
|
||||
| `port/src/libultra.c:794–810, 1108` | the three dead seams this plan fills |
|
||||
| `port/src/dram.c`, `port/shim/PR/R4300.h` | address model (V1/V2 views) + the K0_TO_PHYS gap |
|
||||
@@ -0,0 +1,125 @@
|
||||
# AUDIT-M6 — `struct player` / `struct hand` raw-offset pass
|
||||
|
||||
Survey only (Track 3 of `docs/PLAN-M6-playable.md`). No source changed, no
|
||||
build. Feeds a follow-up fix session. Companion to §F D98/D100/D101/D102.
|
||||
|
||||
## Method
|
||||
|
||||
Grep of `src/` for: `handoffset`, `(u8 *)g_CurrentPlayer`, `g_CurrentPlayer + 0x`,
|
||||
`+ 0x[3-hex]`, `sizeof(struct hand|player)`, `* sizeof(struct`, hardcoded
|
||||
`0x2A80`/`0x2A70`, `->hands[i] +`, punned `->m[` / inline-Model accesses,
|
||||
`bondheadmatrices`, `copy_of_body_obj_header`, `field_59C..field_6CC`,
|
||||
`field_654`, `gaitRwData`. Cross-checked every hit against the live
|
||||
`struct player` / `struct hand` definitions in `src/game/bondview.h`
|
||||
(player @ line 314, hand @ line 119).
|
||||
|
||||
## PC layout facts (from bondview.h + bondtypes.h)
|
||||
|
||||
- `struct hand` (bondview.h:119): N64 `sizeof` ≈ **0x3B8** (base 0x870→0xC28
|
||||
inside `player`). PC adds: `audioHandle` +4, `rocket` (`AttachedObj*`) +4,
|
||||
`mtxlist` (`Mtxf*`) +4, and the D102 `#ifdef PORT` tail
|
||||
`struct Model weaponModel` (~0x2A8) + `u32 weaponRwPool[192]` (0x300).
|
||||
**PC `sizeof(struct hand)` ≈ 0x968** — roughly **+0x5B0** vs N64, and *not*
|
||||
a clean multiple, so `handnum * sizeof(struct hand)` de-syncs for `handnum==1`.
|
||||
- `struct player` pointer growth **below N64 offset 0x594** (contradicts the
|
||||
D100 note that “anything below 0x594 is unaffected”): `cameratile` (0x34),
|
||||
`prop` (0xA8), `bodyModel` (0xD4), `autoaim_target_y` (0x130),
|
||||
`autoaim_target_x` (0x140) → **+0x14** by offset ~0x144. `field_10C4`..
|
||||
and the `bloodImg*` / `InvItem*` / `textoverride*` blocks add much more
|
||||
higher up.
|
||||
- `model` (N64 0x598): D100 made it an **inline `struct Model` (~0x2A8)**
|
||||
replacing an 8-byte pointer → **+0x2A0** for everything after it, before
|
||||
`field_59C..field_6CC` (which D100 grep-verified dead except `field_654`).
|
||||
- `ptr_hand_weapon_buffer[2]` +0x8; `copy_of_body_obj_header[2]` — inline
|
||||
`ModelFileHeader` grew 0x20→0x30 (4 pointers ×+4) → **+0x20**;
|
||||
`item_related[2]` `struct texpool` pointer growth; `bondheadmatrices` /
|
||||
`viewports` unchanged.
|
||||
- Net delta from `player` base to `hands[]`: **≈ +0x2E0** (0x14 + 0x2A0 +
|
||||
0x08 + 0x20 + texpool). So N64 `hands[0]` @ 0x870 is at PC **≈ 0xB50**;
|
||||
N64 `hands[0].throw_item_pos_related` @ 0x870+0x268 = **0xAD8** is at PC
|
||||
**≈ 0xDB8**.
|
||||
|
||||
## Findings table
|
||||
|
||||
| # | file:line | accessor | targets (N64 field) | N64 off | PC off (real) | PORT-adj? | BUNKER1 FP reachable? | severity | recommended fix |
|
||||
|---|-----------|----------|---------------------|---------|---------------|-----------|----------------------|----------|-----------------|
|
||||
| 1 | `src/game/gunfire.c:4960` `THROWMTX` (macro) used @ 5043,5047,5070,5126,5360,5364,5387,5443 | `(Mtxf*)((u8*)g_CurrentPlayer + handoffset + 0xAD8)` (`0xAD0` EU) | `hands[handnum].throw_item_pos_related` | 0xAD8 (h0) / 0xAD8+0x3B8 (h1) | ≈0xDB8 (h0) / ≈0xDB8+0x968 (h1) | **NO** | **YES** — `sub_GAME_7F068508` runs on every shot with a cartridge (PP7/KF7/…), solo only (`getPlayerCount()>=2` early-out) | **HIGH — memory corruption.** Raw 0xAD8 on PC lands inside the inline `model` / `field_59C` region of `struct player`; `matrix_4x4_copy(THROWMTX,…)` writes 64 bytes there. `handnum==1` (dual wield) additionally uses the wrong stride. Corrupts the gait `Model` / bondhead matrices → wrong Bond-body render or a delayed fault in model code. | Replace macro body with `(&g_CurrentPlayer->hands[handnum].throw_item_pos_related)`. Drop `THROWMTX_OFFSET`, `handoffset`. |
|
||||
| 2 | `src/game/gunfire.c:4961` `THROWPOS(k)` used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 | `((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB08))[k]` (`0xB00` EU) | `hands[handnum].throw_item_pos_related.m[3][k]` (translation row) | 0xB08 | ≈0xDE8 | **NO** | **YES** (same path as #1) | **HIGH — silent-wrong + reads corrupted memory.** Feeds `casing->vel` from the frame-to-frame delta of a garbage location → casings fly wrong / NaN, and the read is OOB-ish relative to intent. | `g_CurrentPlayer->hands[handnum].throw_item_pos_related.m[3][k]` |
|
||||
| 3 | `src/game/gunfire.c:4962` `THROWPREV(k)` used @ 5112-5114, 5164-5166, 5429-5431, 5481-5483 | `((f32*)((u8*)g_CurrentPlayer + handoffset + 0xB48))[k]` (`0xB40` EU) | `hands[handnum].throw_item_pos_related_prev.m[3][k]` | 0xB48 | ≈0xE28 | **NO** | **YES** (same path) | **HIGH** (same as #2) | `g_CurrentPlayer->hands[handnum].throw_item_pos_related_prev.m[3][k]` |
|
||||
| 4 | `src/game/gunfire.c:5031` and `:5348` | `handoffset = handnum * sizeof(struct hand)` | array stride for `hands[handnum]` | 0x3B8 | 0x968 (and non-uniform) | **NO** (the raw math is only consumed by #1–#3) | YES | **HIGH** — root of #1–#3’s `handnum==1` error; harmless once #1–#3 use `hands[handnum].field`. | delete; fold into the field accessors above. |
|
||||
| 5 | `src/game/gunfire.c:600-661` weapon-model setup (`copy_of_body_obj_header[handnum]` → `modelInit(HAND_WEAPON_MODEL, mdlhdr, HAND_WEAPON_RWPOOL)`; `hand->mtxlist = rwmtx`; `render_pos = (RenderPosView*)rwmtx`) | field-name access + D102 macros | `hands[h].weaponModel` / `weaponRwPool` / `mtxlist`; `copy_of_body_obj_header[h]` (inline `ModelFileHeader`) | — | — | **YES (D102)** for the Model/pool pun; `copy_of_body_obj_header` is a plain inline struct (layout-consistent) | YES — this *is* the 1P weapon draw path | **MED — prime suspect for “weapon model doesn’t draw.”** `rwmtx` is `dynAllocate`’d (transient per-frame arena) but `weaponModel.render_pos` is set to it once at setup and consumed at render (`gunRenderFirstPersonGunModels:1605`); on N64 `render_pos` aliased the *persistent* `mtxlist` field, here it can point at reused arena memory. Also `flashvisptr = HAND_WEAPON_RWPOOL + node->…RwDataIndex` (gunfire.c:640) assumes N64 word indices into a pool whose PC records are ~2×. | Verify `dynAllocate` lifetime vs. render; consider persisting the matrix list. Re-check `weaponRwPool[192]` sizing against `modelCalculateRwDataLen(mdlhdr)` on PC. Confirm `subdraw` matrix path (shared with Track 2). |
|
||||
| 6 | `src/game/bondview2.c:3155-3162` (PORT branch of `sub_GAME_7F07E7CC`) | `(Model*)&g_CurrentPlayer->something_with_watch_object_instance`; `static u8 watchRwPool[0xC8]`; `g_CurrentPlayer->step_in_view_watch_animation = 0` | watch-menu item preview Model punned at N64 player+0x230, pool at +0x2EC | 0x230 / 0x2EC | 0x244 / 0x300-ish | **YES (D56)** but suspect | YES — open watch menu / cycle to an item page | **MED — under-reservation.** D56 hosts the pool in a `0xC8` static (N64 capacity) but PC RW records are wider; and `animInit` writes a full inline `struct Model` (~0x2A8) starting at `&something_with_watch_object_instance`, overrunning `field_234..watch_scale_destination..` (N64 those were the tail of the 0xBC Model hole; on PC they are live watch fields). | Give the watch Model real inline storage in `struct player` under `#ifdef PORT` (D100 pattern) + size `watchRwPool` from `modelCalculateRwDataLen`. |
|
||||
| 7 | `src/game/gunfire.c:4951-4957` | `#define THROWMTX_OFFSET 0xAD8` etc. | (the constants behind #1–#3) | — | — | NO (EU/non-EU split only, both N64) | — | INFO — remove with #1–#3. | delete block. |
|
||||
| 8 | `src/game/player.c:136` | `mempAllocBytesInBank((sizeof(struct player)+0xF)&~0xF, …)` | player allocation | — | — | **YES (D98)** | — | OK — listed for completeness; correct. | none |
|
||||
| 9 | `src/game/initBondDATAdefaults.c:103` | `animInit(&g_CurrentPlayer->model, &player_gait_object_header, g_CurrentPlayer->gaitRwData)` | inline gait Model + `gaitRwData[256]` | — | — | **YES (D100)** | YES | OK. | none |
|
||||
| 10 | `src/game/bondhead.c:217-480`, `bondview2.c:922-9102` | `&g_CurrentPlayer->model`, `bondheadmatrices[0].m[..]` (field-name) | inline gait Model + head matrices | — | — | **YES** (D100 makes field access correct; single TU layout) | YES | OK — no raw offsets; safe as long as D100 inline-Model stays. | none |
|
||||
|
||||
### Not player/hand but same class (noted, out of scope for the fix list)
|
||||
|
||||
- `src/game/gunfire.c:5513` `render_data.mtxlist = (Mtxf*)model_matrices` — local, fine.
|
||||
- `initexplosioncasing.c`, `initobjects.c`, `propobj.c`, `glass2.c`,
|
||||
`initunk_005520.c` — all `N * sizeof(struct X)` allocations, already
|
||||
`sizeof`-correct (D57 covered the rwdata capacities).
|
||||
- `ramromreplay.c` `+0x110/+0x1F8/+0x21E` — `ramromfilestructure`, handled by D66/D87, unrelated.
|
||||
|
||||
## Total
|
||||
|
||||
**10 distinct offset sites; 3 live HIGH-severity bugs (findings #1–#3, one
|
||||
underlying cause #4), 1 dead constant block (#7), 2 MED items to verify
|
||||
(#5, #6), 4 already-correct (#8–#10).**
|
||||
|
||||
All 3 HIGH bugs are the same construct in `gunfire.c` and are reachable in
|
||||
BUNKER1 first-person the instant the player fires a weapon that ejects a
|
||||
casing (every ballistic weapon). They are NOT reachable before the first
|
||||
shot, so they do not explain a weapon model that is missing from frame 1 —
|
||||
but once firing starts they corrupt the inline gait `Model` region and will
|
||||
produce cascading render/anim breakage or a fault.
|
||||
|
||||
## Where the “1P weapon model doesn’t draw” bug most likely lives
|
||||
|
||||
Not in a surviving raw offset — D102 already gave the weapon `Model` and its
|
||||
RW pool dedicated storage, and `gunRenderFirstPersonGunModels` /
|
||||
`gunTickHandState` use field names. Best candidates, in order:
|
||||
|
||||
1. **Finding #5 — the D102 wiring in `gunfire.c:600-661`.** `weaponModel.render_pos`
|
||||
is pointed at a `dynAllocate`’d transient (`rwmtx`) that on N64 aliased the
|
||||
*persistent* `hand->mtxlist` field; if the arena is recycled between the
|
||||
hand-tick setup and `gunRenderFirstPersonGunModels`, the model draws with
|
||||
garbage / off-screen matrices (looks like “not drawn”). Check `dynAllocate`
|
||||
lifetime and whether `weaponModel` should own its matrix list on PC.
|
||||
2. **The `subdraw` / `subcalcmatrices` / `process_02_position` matrix
|
||||
composition** — shared with Track 2 (inverted characters). A handedness /
|
||||
lookat-sign error there hides the weapon model just as it inverts guards.
|
||||
3. **`field_87F` gating** (`gunfire.c:520`, `:587-598`) — if the setup branch
|
||||
that sets `field_87F = 1` and calls `modelInit` isn’t taken (e.g.
|
||||
`get_ptr_weapon_model_header_line(item)` returns NULL because
|
||||
`ptr_hand_weapon_buffer` / weapon-model load failed), the render loop
|
||||
`continue`s and nothing is emitted. Cheap to instrument.
|
||||
|
||||
## Recommended fix order
|
||||
|
||||
1. **#1–#4 & #7 (gunfire.c THROW macros)** — one mechanical change, no layout
|
||||
work: rewrite `THROWMTX`/`THROWPOS`/`THROWPREV` as
|
||||
`g_CurrentPlayer->hands[handnum].throw_item_pos_related[_prev]` field
|
||||
accesses (`.m[3][k]` for the POS/PREV variants), delete `handoffset` and
|
||||
the `THROW*_OFFSET` defines. Wrap in `#ifdef PORT` keeping the N64 macro
|
||||
for the N64 build (or, since the field access is identical semantics on
|
||||
both, use it unconditionally — verify byte-match). Log as §F “D115”.
|
||||
2. **#5** — investigate the weapon-model `render_pos` / `mtxlist` lifetime and
|
||||
`weaponRwPool` sizing; this is the likely playability blocker. May fold
|
||||
into the Track-2 matrix work.
|
||||
3. **#6** — give the watch preview Model inline PORT storage + sized pool
|
||||
(only matters once the player opens the watch in-level).
|
||||
4. Add a debug assert in `gunfire.c` weapon-model setup logging
|
||||
`mdlhdr->numRecords` vs `weaponRwPool` capacity and `field_87F` state, to
|
||||
settle candidate 3 above.
|
||||
|
||||
## §F pointer
|
||||
|
||||
Add to `docs/internals.md` §F after D112: *“**D115 (audit)** —
|
||||
`struct player`/`struct hand` raw-offset pass: see
|
||||
`docs/dev/AUDIT-M6-player-offsets.md`. Live HIGH bugs = `gunfire.c` THROWMTX/POS/PREV
|
||||
macros (`+handoffset+0xAD8/0xB08/0xB48`), not PORT-adjusted; fix = field
|
||||
accessors. MED = D102 weapon `render_pos` transient-arena aliasing (likely
|
||||
the ‘weapon model doesn’t draw’ cause) and the D56 watch-Model pool
|
||||
under-reservation.”*
|
||||
@@ -0,0 +1,66 @@
|
||||
# GE_* environment probes — consolidated list
|
||||
|
||||
Every `getenv("GE_…")` in the tree, from a grep of `src/` + `port/` on
|
||||
2026-08-31 (master `31d036da`). All are **env-gated**: unset = zero behaviour
|
||||
change, so they are safe to leave in a release build (they only cost a
|
||||
`getenv` on the guarded path). "Dead" below means *the finding it was built
|
||||
for is closed* — the probe is strip-candidate scaffolding, not that it does
|
||||
anything harmful.
|
||||
|
||||
Two classes:
|
||||
- **Infra / live tooling** — deliberately kept, used by the playtest harness
|
||||
(`tools_pc/debug.ps1`, `playtest.sh`, `level_sweep.sh`) or by an
|
||||
investigation still in progress.
|
||||
- **Dxx diagnostic** — added to chase one finding; most are now closed.
|
||||
|
||||
## Infra / live tooling
|
||||
|
||||
| Env var | File:line | What it does | Status |
|
||||
|---|---|---|---|
|
||||
| `GE_PCDUMP` | `port/fast3d/gfx_opengl.cpp:1079` (`is_pcdump`), `gfx_opengl.cpp:700`, `port/src/video.c:188`, `port/src/config.c:46` (`configGetFrameDump`) | Per-frame framebuffer PPM dump to `$PCDUMP` dir (verification-ritual golden captures, `tools_pc/framediff.py`/`pixcount.py`). Also settable via `[Debug] FrameDump` ini (env wins). | **live** |
|
||||
| `GE_INPUTSCRIPT` | `port/src/input.c:306` | Headless scripted controller-0 input: `"<frame>:<tok>,…;…"` (`SUP/SDOWN/SLEFT/SRIGHT/SNONE` sustain, buttons pulse). Sole input source when set. | **live** |
|
||||
| `GE_INPUTLOG` | `port/src/input.c:329`, `port/src/config.c:53` (`configGetInputLog`) | Logs computed pad state (buttons + stick) each poll when non-zero. Also `[Debug] InputLog` ini. Also traces D165 `menuptr est/tgt/eff/stick`. | **live** |
|
||||
| `GE_STARTMENU` (+ `GE_STARTMENU_PAGE`, `GE_STARTMENU_DIFF`) | `src/game/lv.c:398-402` (`#ifdef PORT`) | Boot straight into a front-end menu id (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT); `_PAGE`=folder row (def 1=Dam), `_DIFF`=0..3. Crash-test a screen fast. | **live** |
|
||||
| `GE_UNLOCK_ALL` | `src/game/debugmenu_handler.c:1098` (`get_debug_enable_all_levels_flag`, `#ifdef PORT`) | Mission-select shows every solo level (playtest jump-to-any-level aid). Cached in a `static int c`. | **live** |
|
||||
| `GE_SAVELOG` | `src/game/file.c:46`, `src/game/file2.c:16` (`SAVELOG` macro), `src/boss.c:749`, `src/game/objective_status.c:297,343` | Traces the campaign save/unlock chain (objective completion → `end_of_mission_briefing` → `fileWriteSave` → EEPROM). Used to confirm D157. `g_savelogObjOnce` gates the per-criterion dump to the `bossReturnTitleStage` call. | **live** (strip once a few more level boundaries are playtested) |
|
||||
| `GE_D160` | `src/boss.c:744`, `src/game/bondview2.c:791`, `src/aicommands.def:10055,10181,10236,10242,10247` (all `#ifdef PORT`) | Dam exit-cutscene (D148/D160) diagnostic: trace points in `bossReturnTitleStage`, `bondviewSetCameraMode`, and AI cmds `EndLevel`/`exit_level`/`CameraLookAtBondFromPad`/`CameraSwitch`. **Investigation in progress** — user owes a `GE_D160=1` Dam run. | **live** |
|
||||
| `GE_DTEX` | `port/fast3d/gfx_pc.cpp:1001` | Per-`import_texture` param log (dims / line / size / lod / gen_mipmaps). Used to root-cause RC2 and D159. Kept as an inert diagnostic. | **live** (RC2/RC3 not finished) |
|
||||
| `GE_TEXDUMP` | `port/fast3d/gfx_pc.cpp:1020`, `port/fast3d/gfx_opengl.cpp:700` | PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` line. Root-caused D161 (Depot ceiling). Kept as an inert diagnostic. | **live** (RC3 not finished) |
|
||||
| `GE_D116` | `port/fast3d/gfx_pc.cpp:1710,2362`, `src/game/textrelated.c:265,541` (cached `ge_d116`) | HUD/text X-mirror (D114/D116) vbo/uv trace. Bug is **parked / DO-NOT-re-static-trace** (PORT-LEARNINGS §D2) but the probe is cheap (cached int) — leave until that cosmetic bug is picked up with RenderDoc. | idle (parked bug) |
|
||||
|
||||
## Dxx diagnostic probes — finding closed unless noted
|
||||
|
||||
| Env var | File:line | Finding / what it logged | Status |
|
||||
|---|---|---|---|
|
||||
| `GE_D51` | `port/src/libultra.c:496`, `src/game/model.c:125,244,544` | msgQ 32-slot overflow watch / ModelSlot layout | dead (D51 closed) |
|
||||
| `GE_D54` | — (all blocks stripped M-32, commit `49ce620a`) | music seq-table ABI / endianness (`ALMidiHdr`) | dead (D54 closed) — fully removed |
|
||||
| `GE_D56` | `src/game/model.c:227,830` | watch `Model` raw-offset reads | dead (D56 closed) |
|
||||
| `GE_D60` | `port/src/libultra.c:521,735` | gfx frame msgQ delivery trace | dead (D60 closed) |
|
||||
| `GE_D61` | `port/src/libultra.c:795` (`s_d61opened`) | opens a one-shot log file | dead (D61 closed) |
|
||||
| `GE_D62` | `port/src/libultra.c:477` | osRecvMesg trace | dead (D62 closed) |
|
||||
| `GE_D63` | remaining: `src/game/bg.c:2525,2908,2953`, `front.c:1410,1416,7814,7889,8083,8091,8099,8366`, `image.c:2453`, `language.c:431`, `model.c:4340`, `title.c:607` | gun-barrel sub-DL clobber hunt (D63/D64/D65/D66 — the labels drift within the blocks). Stripped M-30 (`d0789358`, `gfx_pc.cpp` trail). Stripped M-32 (`49ce620a`): `memp.c`, `bg.c:2880` (incl. the bare non-PORT `d63bgprimarycount` static + entry log), `blood_animation.c`, `dyn.c`, `rsp.c`. **Remaining blocks are all `#ifdef PORT` + getenv-guarded and inert — strip candidates for a later pass.** | dead (D63 closed) — partially removed |
|
||||
| `GE_D69` | `src/game/ob.c:165,227` | stage-load (D69) object trace | dead (D69 closed) |
|
||||
| `GE_D69BB` | `src/game/bg.c:2460,2479,3020` | D69 bg-binary layout trace | dead |
|
||||
| `GE_D69STAN` | `src/game/stan.c:266,276` | D69 stan-tile trace | dead |
|
||||
| `GE_D85DUMP` | `src/game/bg.c:2510` | D85 `bgWidenRoomGdl` 8→16B dump | dead (D85 closed) |
|
||||
| `GE_D85TEX` | `src/game/image.c:2479`, `src/game/tex.c:859,1050` | D85 texpool-full events (Depot analysis) | dead (D85 closed) — was useful for §3 |
|
||||
| `GE_D86` | `src/game/model.c:6295`, `src/game/objecthandler_2.c:143` | D86 model rwdata trace | dead (D86 closed) |
|
||||
| `GE_D87` | `src/game/ramromreplay.c:296,363,381` | D87 `ramromfilestructure` endianness | dead (D87 closed) |
|
||||
| `GE_D88` | `src/game/prop.c:1367`, `src/game/stan.c:3090` | D88 `Usetup*Z` propDef stream | dead (D88 family closed; D88.4 resolved) |
|
||||
| `GE_D90` | remaining: `src/game/bondview2.c:2146`, `src/game/prop.c:1391` (bondview_r.c blocks stripped M-32, `49ce620a`) | D90 bondview / prop NULL trace | dead (D90 closed) — partially removed |
|
||||
| `GE_D96` | `src/game/chrprop.c:436,507` (cached `probe`) | D96 chrprop trace | dead (D96 closed) |
|
||||
| `GE_D104` | `src/game/bg.c:654` (rate-limited, `d104c`) | D104 depth-clear trace | dead (D104 closed) |
|
||||
| `GE_D71LOG` | `port/fast3d/gfx_pc.cpp:645` | D71 fast3d trace | dead (D71 closed) |
|
||||
| `GE_D75` | `src/game/title.c` `sub_GAME_7F007F30` (capped 8) | D75 Bug 2 gun-barrel model probe: logs `chrModelInstance`/`gunModelInstance` ptr+obj+numMatrices, `render_pos`, `renderData.mtxlist`, `g_GfxMemPos`, `osVirtualToPhysical(render_pos)` | **LIVE** (D75 Bug 2 OPEN — see §F "D75 Bug 2 — RUNTIME PROBE") |
|
||||
|
||||
## Notes
|
||||
|
||||
- The bare non-`#ifdef PORT` `GE_D63` block in `bgRenderRoomPrimary`
|
||||
(`d63bgprimarycount` static + entry log, was ~`bg.c:2876/2880`) was
|
||||
removed M-32 (`49ce620a`). Every remaining `GE_D63` block is
|
||||
`#ifdef PORT` / `#if defined(PORT)` wrapped and inert.
|
||||
- `[Debug]` ini keys (`Debug.FrameDump`, `Debug.InputLog`) mirror
|
||||
`GE_PCDUMP` / `GE_INPUTLOG` as a fallback (env var wins) — M-26,
|
||||
`port/src/config.c`.
|
||||
- Not GE_-prefixed but same category: `GE_DETERM` is **designed, not
|
||||
implemented** (fixed-tick determinism mode, §F D117).
|
||||
@@ -0,0 +1,27 @@
|
||||
# GRAPHICS-BACKLOG — known cosmetic / rendering issues
|
||||
|
||||
Deferred below level-progression and crash work (user priority, M-11).
|
||||
Minor graphical glitches are acceptable in the current state. Fix these
|
||||
once the game is playable start-to-finish without crashes.
|
||||
|
||||
## Open
|
||||
|
||||
| ID | Symptom | Status / notes |
|
||||
|----|---------|----------------|
|
||||
| **D114/D116** | HUD text + ammo digits render each glyph individually X-mirrored (screen positions correct, texture content flipped). Likely also explains "inverted" guard skins / mislocated door props. | Root cause NOT found after 4 sessions (M-6/7/8/11). Every stage `textRenderGlyph`→GBI→fast3d→`buf_vbo`→GL runtime-verified non-mirrored (`GE_D116` `[D116/vbo]` probe). Contradiction unresolved. **Next attempt requires a RenderDoc/apitrace capture of one glyph texrect, or the asymmetric-1-texel-texture experiment** — do NOT re-run the static trace, and do NOT apply a global texrect S-swap (mirrors the whole screen). |
|
||||
| **D74 wrap-block** | `gfx_pc.cpp:1546/1551` PORT block is dead code: guard `cms & G_TX_WRAP` with `G_TX_WRAP==0` is always false; and it indexes per-texunit `[2]` arrays with the vertex index `i` (0..2) → OOB if reached. | Latent. Harmless for CLAMP glyphs; could affect wrapped textures on triangles (room panels, model skins). Fix: `cms == G_TX_WRAP`, and compute the tile window once per tile not per vertex. Verify against `tools_pc/golden/` after. |
|
||||
| **D75** | Nintendo logo mispositioned; gun-barrel Bond model absent; cast-roll character models absent. (Rareware logo OK.) | Front-end 3D model path. Partially overlaps D114 (shared mirror on asymmetric content). Triage plan in `findings.md` §F D75. |
|
||||
| **D76** | 2D disclaimer screen only partially drawn. | §F D76. |
|
||||
| **D77** | No audible music on PC. | Phase 3 (audio). §F D77. |
|
||||
| **D116 (HUD)** | (subset of D114) proportional-font strings + ammo counter mirrored. | Same root cause as D114. |
|
||||
| **D148** | Level-end cinematics don't play. Dam: hitting the exit trigger should show Bond rappelling down the dam (the bungee jump); instead it cuts straight to the mission-complete report. | Scripted cutscene / cinematic-camera + Bond rappel anim. Found M-27 during the first full Dam→Facility playthrough. Progression is unaffected — the report and next briefing load fine. **M-31 (§F D160):** static-traced the Dam end ailists (`ai_24`→`ai_17`, `camera_switch`→`CutsceneRecord` type-46 lookup). Diagnostic `GE_D160=1` shipped (`c95713f5`) — user runs Dam to the exit with it set; the trace pins whether the cutscene AI runs, and whether the propDef-index walk (D122/D132 family) hands `camera_switch` a bogus cutscene record. Leading hypothesis: propDef `sizepropdef` stride desync (medium confidence), not D75 anim-model. |
|
||||
| **D149** | Front-end MISSION COMPLETE / mode-select 3D models (dossier, wallets) render as garbled full-screen geometry or not at all — one of the compiled sub-DLs (seg5+0x9ee4) is malformed, D146 now skips it instead of aborting. | D75 family. §F D146. |
|
||||
|
||||
## Resolved (for reference)
|
||||
|
||||
- D70–D74: intro-logo pixels (Rareware logo renders correctly).
|
||||
- D103: viewport / native-resolution half-frame.
|
||||
- D105: room geometry Z-buffer never cleared (`G_CLEAR_DEPTH_EXT`).
|
||||
- D106: sky-void through doorways (portal near-plane min>max).
|
||||
- D107: blurry ceilings/wall-panels (LOD mip tile fabrication).
|
||||
- D112: skeletal models were "3D lines" (`d43_emit.py put_f32` byte order).
|
||||
@@ -0,0 +1,520 @@
|
||||
# HANDOFF ARCHIVE — session-by-session narrative
|
||||
|
||||
> **Reference only.** Current state, next task, and environment live in
|
||||
> `docs/HANDOFF.md`; per-finding detail in `docs/internals.md` §F/§H
|
||||
> (indexed at the top of §F). This file is the frozen accumulation of
|
||||
> prior sessions' handoff briefs (through session M-11), kept so the
|
||||
> reasoning behind a fix is recoverable. Everything below is as-written
|
||||
> at the time and may be superseded.
|
||||
|
||||
---
|
||||
|
||||
# Handoff brief — GoldenEye 007 PC port (Phase 2: Session M-3 —
|
||||
# STAGE-LOAD → RENDER crash chain CLEAR; BUNKER1 renders, next is D75 model quality)
|
||||
|
||||
## PREFLIGHT — before touching anything (a concrete next-step below is NOT a reason to skip this)
|
||||
|
||||
1. `CLAUDE.md` (auto-loaded) + `AGENTS.md` non-negotiables.
|
||||
2. This file, top-to-bottom.
|
||||
3. `docs/porting-notes.md` — recurring bug classes.
|
||||
4. Dispatching a subagent? `docs/dev-process.md` FIRST. Every brief needs
|
||||
FILES / BUDGET / ON-EXPIRY / CONSTRAINTS / REPORT (see CLAUDE.md "Dispatch preflight").
|
||||
|
||||
_Paste-ready brief. Authoritative context: `AGENTS.md`,
|
||||
`docs/internals.md` §F (D69, D78-D102), `docs/BRIEF-D69-stage-load.md`._
|
||||
|
||||
**NEW DIRECTION (2026-08-29): `docs/PLAN-linear-level-sweep.md`** — pivot
|
||||
to breadth-first: fix the boot→intro→menu→level-select path to
|
||||
*functional, not crashing* (cosmetics parked in `GRAPHICS-BACKLOG.md`),
|
||||
then sweep all 21 solo levels for load+render+no-crash. Start with WS1
|
||||
(auto-inject `memallocstringtable` args for bare `-level_XX`).
|
||||
|
||||
## READ THIS FIRST — crash chain CLEAR; viewport fixed; room geometry is next
|
||||
|
||||
`-level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150` **boots BUNKER1
|
||||
and renders continuously with no fault** (60 s+, 5000+ VI posts at full
|
||||
framerate; attract mode also clean). The stage-load → first-frame →
|
||||
in-level crash chain (blocker since D69) is resolved.
|
||||
|
||||
**D103 (session M-4) fixed the "~46 %, lower half" symptom** — it was a
|
||||
single native-resolution/viewport bug (`osViSetMode` hard-coded height
|
||||
480 → `RATIO_Y` half of `RATIO_X`), not per-model. Frame is now full
|
||||
(91.7 %, correct letterbox + HUD placement). See §F "D103".
|
||||
|
||||
**D105 (session M-4) fixed room geometry** — `zbufClearCurrentPlayer`
|
||||
(`viewport.c`) used the N64 "fill the Z buffer as a colour image" idiom,
|
||||
which fast3d doesn't emulate, and nothing emitted `G_CLEAR_DEPTH_EXT`, so
|
||||
the depth buffer was never cleared and all ~160 k room triangles failed
|
||||
the Z test (only `skyRender`'s background fill survived). PORT branch now
|
||||
emits `G_CLEAR_DEPTH_EXT`. See §F "D104"/"D105". BUNKER1 now renders
|
||||
recognisably — textured walls, storage racks, floor (2585 colours, sky
|
||||
16 %), 70 s crash-free.
|
||||
|
||||
**D106 (session M-4)** — the big sky-void through doorways was the portal
|
||||
BFS dropping the next room: a portal straddling the camera near-plane
|
||||
projects z==0 clip points to ±1e20 screen coords, and on x86-64 that
|
||||
garbage came back `min>max` on one axis, slipping past the
|
||||
degenerate-box check that on N64 clamps it to full-screen. PORT guard in
|
||||
`sub_GAME_7F0B5864` treats non-finite / out-of-range bounds as
|
||||
full-screen. Visible rooms/frame 1–3 → 2–4. See §F "D106".
|
||||
|
||||
**D107 (session M-4)** — blurry ceilings/wall-panels were fast3d sampling
|
||||
GE's first mip (render tile 1), whose single-`G_LOADBLOCK` TMEM slot
|
||||
fast3d never registers, so `import_texture` fabricated a 16×16 crop of
|
||||
the base image and magnified it. `gfx_lod_tile_offset` now returns 0
|
||||
(base tile) when detail textures are off. BUNKER1 rooms render crisp.
|
||||
See §F "D107".
|
||||
|
||||
**Skeletal models — FIXED (session M-5, D112).** The "3D line" / missing
|
||||
characters were `tools_pc/d43_emit.py`'s `put_f32` reversing the wrong 4
|
||||
bytes (`src[doff+4:doff:-1]` → bytes doff+1..doff+4, dropping the MSB),
|
||||
corrupting every f32 in converted model rodata (joint `Origin`s, LOD
|
||||
distances, BSP planes). Compiled-in front-end models were unaffected
|
||||
(native LE), hence "logo fine, every guard broken". Fix: one line,
|
||||
`src[doff:doff+4][::-1]`; regen with `python tools_pc/d43_emit.py
|
||||
ntsc-final`. BUNKER guards now render as coherent humanoids, monitor
|
||||
screens draw content. See §F "D108–D112".
|
||||
|
||||
**"props emit no geometry" was a STALE premise** — props/doors emit
|
||||
complete leaf DLs (~15k/run, valid ptrs; fast3d transforms them). The
|
||||
storage-room void is NOT closed doors failing to render.
|
||||
|
||||
**Priority (user, M-11): get BUNKER1 — then every level — playable
|
||||
start-to-finish with no crashes. Fix crashes/hangs first; cosmetic
|
||||
rendering (D74/D75/D76/D114/D116) is logged in `docs/GRAPHICS-BACKLOG.md`
|
||||
and comes later.** D120 (blood-stain converter) is the nearest real
|
||||
converter gap; extending `d43_emit.py` for opcode-0x18 is a good
|
||||
subagent brief. After BUNKER1: walk the objective/exit path to a level
|
||||
transition, then the next stage.
|
||||
|
||||
**Real remaining work, in order (updated session M-8):**
|
||||
1. **Input layer (Phase 3)** — IN PROGRESS (M-8 agent, §F D118). `port/src/
|
||||
input.c` was a stub; only a minimal keyboard path in `libultra.c`
|
||||
(`contSnapshotFromKeyboard`) worked. No C-buttons/mouse-look/gamepad/
|
||||
rebinding. This is the top playability blocker — you cannot aim or
|
||||
properly move. Reference: `pd_port/port/src/input.c`.
|
||||
2. **Weapon model doesn't draw (AUDIT-M6 #5)** — NOT STARTED (M-8 agent
|
||||
hit the account session rate-limit before any work; tree untouched).
|
||||
Plan: instrument `gunfire.c` weapon setup (~600-661) + `field_87F`
|
||||
gating (~520/587-598) FIRST with a `GE_D119` printf — does setup even
|
||||
run? Then check `weaponModel.render_pos` = `dynAllocate`'d `rwmtx`
|
||||
lifetime vs the `gunRenderFirstPersonGunModels` (~1605) consumer (on
|
||||
N64 it aliased the persistent `hand->mtxlist`; D102 gave it own
|
||||
storage → arena may recycle). Also `weaponRwPool[192]` capacity vs
|
||||
`modelCalculateRwDataLen(mdlhdr)` on PC, and `flashvisptr` word-index
|
||||
assumption (~640). Narrow `#ifdef PORT` lifetime/sizing fix only — if
|
||||
it needs a behavioural arena change, STOP + write up. Direct `-level_09`
|
||||
boot may spawn Bond holstered — may need forced weapon state or the
|
||||
attract path to get the gun on screen. Gate to playability.
|
||||
3. **The HUD/text X-mirror (D116)** — DEPRIORITISED, parked in
|
||||
`docs/GRAPHICS-BACKLOG.md`. M-11 confirmed the ammo digits use the
|
||||
SAME `textrelated.c` path (no separate renderer — the M-8 premise was
|
||||
wrong) and re-hit the same contradiction: every stage verified
|
||||
non-mirrored, glyphs still flip. **Do not re-static-trace.** Next
|
||||
attempt needs a RenderDoc/apitrace capture or the asymmetric-1-texel
|
||||
experiment — nothing else. All cosmetic rendering issues (D74 dead
|
||||
wrap-block, D75/D76/D77) now tracked in `docs/GRAPHICS-BACKLOG.md`;
|
||||
they rank BELOW level-progression and crash work.
|
||||
4. **Rest of the `struct player` offset pass** — D115 fixed the HIGH
|
||||
`gunfire.c` THROW* bugs; #6 (watch-preview Model pool) + the broader
|
||||
audit remain (`docs/dev/AUDIT-M6-player-offsets.md`).
|
||||
5. Add an f32 value spot-check to `d43_emit.py`'s verify pass (it only
|
||||
checks pointers/opcodes today — the D112 bug passed "ALL CHECKS PASSED").
|
||||
6. **GE_DETERM fixed-tick mode (§F D117)** — deferred as not-narrow; would
|
||||
unlock exact-diff regression testing + reproducible timing bugs. Design
|
||||
is written up. Worth doing once the above land.
|
||||
|
||||
**Session M-7 — see §F D116 + "D116 probe results".** Ran the glyph
|
||||
probe (overseer, after killing a subagent that was drifting toward a
|
||||
global texrect S-swap). Corrected finding: the HUD text mirror is a
|
||||
**per-quad texture-U flip that is NOT path-specific** — the ammo digits
|
||||
"83" are mirrored too (`ppm/frame_000320.ppm`), refuting M-7's earlier
|
||||
"proportional-font-specific" claim. Rect *positions* are correct; only
|
||||
texture content is X-flipped. Every fast3d stage probed clean (glyph
|
||||
bitmap in memory correct, texrect `ul.u=0→lr.u=max`, `import_texture_i8`
|
||||
linear, GL shader UV pass-through) — so the flip is in the GL
|
||||
vertex-buffer/draw layer OR a shared clip-space-X vs U desync on rect
|
||||
quads. **This re-opens D114's shared-mirror hypothesis** (now per-quad
|
||||
U/X, not screen-space) and plausibly also explains inverted guards /
|
||||
mislocated door props. NEXT: shader/vertex-buffer probe — dump per-vertex
|
||||
(x,u) for one glyph quad; render a 1-texel asymmetric test texture to see
|
||||
which axis inverts. `GE_D116`-gated probes left in `textrelated.c` +
|
||||
`gfx_pc.cpp` (zero-cost).
|
||||
|
||||
**Session M-8 wrap — state for next session.** Build GREEN at
|
||||
`587c6856`, tree clean. Committed: D116 part-3 writeup + `[D116/vbo]`
|
||||
probe (`5ff012b4`), `CLAUDE.md`/preflight (`fd25628a`), D117
|
||||
framediff+nondeterminism (`44d9ae98`), D118 input layer
|
||||
(`4ac11fe7`/`587c6856`), handoff reprioritisation. `GE_D116` and
|
||||
`GE_INPUTLOG` probes are in-tree, `getenv`-gated, zero-cost. Weapon-model
|
||||
(#5) agent never ran (rate-limit) — task untouched, plan in item 2 above.
|
||||
Pre-existing stash `stash@{0} "d59 probes WIP"` (~300 lines,
|
||||
gfx_pc/gfx_opengl/crash.c) is NOT from M-8 — provenance unknown, left as
|
||||
found. Next: human input playtest (tune `ge007.ini [Input] MouseAimSpeed`),
|
||||
then weapon-model #5.
|
||||
|
||||
**Session M-8 (continued) — D116 part 3 + input layer.** Killed the
|
||||
drifting D116 agent, ran the probe as overseer: `buf_vbo` (x,u) for
|
||||
glyph quads and the GL texture upload are both runtime-verified
|
||||
non-mirrored (x-left<->u=0), GL shader is `gl_Position=aVtxPos` +
|
||||
UV-passthrough — every stage clean, yet glyphs render X-flipped
|
||||
(contradiction, §F "D116 runtime probe part 3"). Deprioritised as
|
||||
cosmetic. `GE_D116` `[D116/vbo]` probe kept in `gfx_pc.cpp`. Then
|
||||
dispatched the **input-layer agent** (§F D118, in progress) — top
|
||||
playability blocker. `framediff.py` structural mode is the interim
|
||||
regression gate (`tools_pc/golden/` is a nondeterministic D115 capture).
|
||||
|
||||
**Session M-8 — see §F D117.** Visual-regression tooling. Root-caused the
|
||||
frame-to-frame nondeterminism: pure variable-timestep frame pacing
|
||||
(`osGetCount()` = wall clock on PC → `frametiming.c waitForNextFrame`
|
||||
advances logic a real-time-dependent number of 60 Hz ticks per render).
|
||||
PRNG seeding verified correct (`random.c`), not a source. A `GE_DETERM=1`
|
||||
fixed-tick mode was assessed **not narrow** (redesigns VI retrace/tick
|
||||
semantics, deadlock risk in load-screen loops) and deferred with a full
|
||||
design in §F D117 — NOT implemented. Added `tools_pc/framediff.py`
|
||||
(structural/tolerant: 16×12 grid mean-colour + non-clear-% + aHash,
|
||||
`--mask` for HUD, `--exact` for a future deterministic build, `--update`
|
||||
to refresh goldens). Validated against the D115 golden set. No C changes,
|
||||
no probes left in tree.
|
||||
|
||||
**Session M-10 — input playtest + guard-firefight crash chain.**
|
||||
|
||||
- **D119 — guard-attack crash FIXED** (`chraction.c`). Every prior
|
||||
`-level_09` run segfaulted ~frame 1200 the instant a BUNKER guard
|
||||
opened fire: `bondwalkItemGetAutomaticFiringRate` (`gun.c:1334`) ←
|
||||
`chrlvInitActAttack`. ~28 sites pun `weapons_held[]->chr` (really a
|
||||
`WeaponObjRecord*`) as `ChrRecord*` and read `.act_<x>.attack_item`,
|
||||
which on N64 aliases `WeaponObjRecord.weaponnum` (act union @0x2C + 84
|
||||
== 0x80). Pointer widening moves the act union to ~0x38 on PC → garbage
|
||||
negative item id → OOB `g_ItemStats` → crash. Fix: `PUN_ATTACK_ITEM()`
|
||||
macro reads `weaponnum` directly; `#else` branch is textually identical
|
||||
to the original → N64 build unchanged. Class-A (D53.2 type-pun) bug.
|
||||
- **D120 — blood-stain hang GUARDED, not fixed** (`chr.c:3322`). Next
|
||||
blocker, reachable only after D119: first guard bullet-hit →
|
||||
`chrCreateBloodStain` `PointUsage[]` negative-terminated chain walk
|
||||
cycles forever (VI thread keeps posting, logic thread spins, kernel
|
||||
heartbeat trips). Root cause: `tools_pc/d43_emit.py`'s opcode-0x18
|
||||
`ModelRoData_DisplayList_CollisionRecord` conversion is incomplete —
|
||||
6 pointer fields widen the struct and `PointUsage`/`CollisionVertices`
|
||||
sub-array endianness+stride aren't handled (only `CollisionRelatedNode`
|
||||
got the D43/D45 `u32`-vma treatment). **Interim:** `#ifdef PORT` caps
|
||||
both walk loops at `numVertices+8` iters + bounds-checks `index` → game
|
||||
survives, blood decals may be missing/wrong. **Real fix (next session,
|
||||
own subagent brief):** byte-spec the opcode-0x18 record + PointUsage +
|
||||
CollisionVertex sub-arrays vs a converted guard model, extend
|
||||
`d43_emit.py`. Same shape as D69/D88 converter work.
|
||||
- Result: `-level_09` now survives the guard firefight — 45 s+
|
||||
crash-free, frames past 1200 (was: hard segfault ~1200 every run).
|
||||
|
||||
**Session M-10 — input playtested (human).** Fixed in `port/src/input.c`:
|
||||
(a) mouse-look was a draining accumulator → flicks stayed "pressed" ~8
|
||||
frames after you stopped (stuck looking up/down) — now per-poll delta,
|
||||
no carryover; (b) horizontal mouse was routed to C-left/right = GE
|
||||
*sidestep*, and A/D drove analog stick-X = GE *turn* — swapped: mouse X
|
||||
→ stick-X turn (`MOUSE_TURN_GAIN 6.0`), A/D → C-left/right strafe, mouse
|
||||
Y → C-up/down look. Core aim/move now works.
|
||||
**Open input bugs (documented, deferred — fix in a later pass):**
|
||||
- **D118a — mouse yaw slower than pitch.** Horizontal turn (analog
|
||||
stick-X via `MOUSE_TURN_GAIN`) is visibly slower than vertical look
|
||||
(digital C-up/down). Different transfer curves (analog rate-limited vs
|
||||
digital full-press). Needs `MOUSE_TURN_GAIN` bump and/or a matched
|
||||
pitch path; ideally a single tunable `MouseAimSpeed`.
|
||||
- **D118b — mouse Y inverted.** Mouse up → looks down, mouse down →
|
||||
looks up (X unaffected). `aimDY`→C-button sign is backwards for GE's
|
||||
pitch convention (or SDL dy sign assumption wrong). Flip the
|
||||
`aimDY >=`/`<=` C-up/C-down assignment (or default `MouseInvertY`).
|
||||
One-line fix once confirmed against GE pitch sign.
|
||||
- Still TODO from D118: rebinding, gamepad hotplug, `ge007.ini` not yet
|
||||
created (config defaults are hardcoded).
|
||||
|
||||
**Session M-9 (Phase 3) — see §F D118.** SDL input layer implemented.
|
||||
`port/src/input.c` is now real: keyboard+mouse and SDL_GameController,
|
||||
mapped to GE's N64 pad (analog stick = move, C-buttons = aim, mouse-look
|
||||
bridged to digital C-buttons via a clamped accumulator, RMB/LT = aim
|
||||
mode, LMB/RT = fire). `libultra.c`'s SI section delegates to it (single
|
||||
source of controller state; still driven by `osContStartReadData`, no
|
||||
new frame hook). Config: `ge007.ini [Input]` MouseEnabled/MouseAimSpeed/
|
||||
MouseInvertY. Build GREEN, boots `-level_09` crash-free 35 s.
|
||||
**Owed: a human playtest** — live input is untested from the headless
|
||||
agent. Manual checklist: (1) WASD moves Bond, mouse turns/looks (tune
|
||||
`MouseAimSpeed` in ge007.ini if too fast/slow), LMB fires, RMB enters
|
||||
aim mode, Enter opens the pause menu; (2) plug an Xbox pad — left stick
|
||||
moves, right stick aims, triggers fire/aim; (3) `GE_INPUTLOG=1` prints
|
||||
each nonzero OSContPad poll. Rebinding + gamepad hotplug are TODO (§F
|
||||
D118).
|
||||
|
||||
**Session M-6 — see §F D113/D114/D115, `docs/PLAN-M6-playable.md`,
|
||||
`docs/dev/AUDIT-M6-player-offsets.md`, `docs/dev-process.md`,
|
||||
`docs/porting-notes.md`.** D113: portal BFS is correct, not the void.
|
||||
D114: matrix chain + converter verified clean, residual = shared fast3d
|
||||
mirror (open). D115: player raw-offset audit + `gunfire.c` THROW* fix
|
||||
(uncommitted). Build green.
|
||||
|
||||
**This session's fixes (all committed, master):**
|
||||
|
||||
| # | commit | one-liner |
|
||||
|---|--------|-----------|
|
||||
| D93 | `164d7f99` | null-room (room 0) NULL-deref guards |
|
||||
| D85 | `493c9838` | `bgWidenRoomGdl` (8→16 `Gfx` + `bswap32`) + `bgSwapRoomVtx`; room DLs decode to real GBI; `bg.c:2448` size-cast bug |
|
||||
| D85 | `6f0208d6` | `ptr_texture_alloc_start` → real `struct texpool` storage (pool looked exhausted → ~630 room textures now resolve; cleared `Bad size for RGBA texture`) |
|
||||
| D94 | `63204a27` | `chrlvInitActAttack` `(s32)`-truncated anim-table index |
|
||||
| D95 | `f35eba91` `933ba52b` | 2× `g_GfxBuffers` (16-byte PC `Gfx`) + raise PC mempool ceiling `0x702F4400→0x70700000` (reclaim ~4 MB DRAM) so it doesn't OOM `MEMPOOL_STAGE` |
|
||||
| D96 | `d86ec483` | `PROPRECORD_STAN_ROOM_LEN` 4→8 (PORT) — prop room-list stack overflow (guards span ≥4 rooms → no terminator → smashed `chrpropsRenderPass` frame; *this* was the "runaway GDL append") |
|
||||
| D97 | `2fbcc556` | clamp negative `damagetype` (US-only OOB `g_DamageTypes[]` read) |
|
||||
| D98 | `000ed6af` | `initBONDdataforPlayer` allocate real PC `sizeof(struct player)` (hardcoded `0x2A80` under-alloc scribbled the master DL) |
|
||||
| D99 | `253caa23` | `Model.animflipfunc` `s32` fn-ptr truncation (flag + direct call, D92 pattern) |
|
||||
| D100 | `8eaad547` | `struct player.model` is an inline `struct Model`, not a `Model*` + 45 filler s32s (PC struct 0x2A8 ≫ 0xB8 hole → `animInit` overran); + dedicated `gaitRwData[]` |
|
||||
| D101 | `b2234f82` | `sub_GAME_7F06DB5C` stashed `ModelNode*`/`RenderPosView*` through an `s32` (the idiom its sibling was already fixed for) |
|
||||
| D102 | `1b078f6d` | 1P weapon `Model` + RW pool punned onto `struct hand` (PC `Model` 0xE8 ≫ N64 0xBC → `modelInit` aliased `datas` onto the pool); dedicated `weaponModel`/`weaponRwPool` |
|
||||
|
||||
**Recurring pattern this session:** the decomp pun-allocates `struct
|
||||
Model` (and `struct player`, `PropRecord`, `struct texpool`, …) into
|
||||
N64-sized holes / hardcoded byte counts. Every one is bigger on x86-64
|
||||
→ overrun → corruption. Fix = give it real inline storage or
|
||||
`sizeof()`-based alloc under `#ifdef PORT`. **Landmine still open**
|
||||
(§F D100): `struct player` / `struct hand` have raw hardcoded-offset
|
||||
accessors (`gunfire.c` `THROWMTX` at `+0xAD8`, …) that are NOT
|
||||
PORT-adjusted — already PC-wrong, a real `struct player` offset pass is
|
||||
owed before grenade/knife-throw code works.
|
||||
|
||||
- Build: `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
|
||||
- Sidecar regen: `python tools_pc/d43_emit.py ntsc-final && python
|
||||
tools_pc/d69_emit.py ntsc-final && python tools_pc/d88_emit.py
|
||||
ntsc-final --regen`. `data/` is gitignored; if missing,
|
||||
`cp baserom.u.z64 data/ge007.ntsc-final.z64` first (§F "`data/`
|
||||
deletion + recovery").
|
||||
- Repro: `./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100
|
||||
-mvtx50 -mt700 -ma150` — `-m*` are BUNKER1's `boss.c` per-level sizes;
|
||||
a bare `-level_09` skips them and crashes early (TODO: auto-inject).
|
||||
|
||||
## Next: D75 — 3D model rendering quality
|
||||
|
||||
BUNKER1 renders but character/skeletal models are wrong (see the D75
|
||||
section further down, still accurate). `GE_PCDUMP` + `tools_pc/pixcount.py`
|
||||
to measure; the animated-model path (`animInit` + `struct player` raw
|
||||
offsets, `drawjointlist`, `modelApplyHeadRelations` head/body splice) is
|
||||
the prime suspect. `struct tex` headers are 24 B vs 16 B on PC — a
|
||||
separate open pool-pressure item; a real PC memory-budget pass is owed.
|
||||
|
||||
## What this session (M-2) fixed — all committed
|
||||
|
||||
1. **`-level_09` now works.** `osPiReadIo` was stubbed to 0, so the token
|
||||
string was always empty and N64 debug switches were ignored (only
|
||||
attract mode could reach a level). Now built from `argv[1..]`.
|
||||
2. **Sidecar tables patched in `obInit()`** instead of lazily at first
|
||||
model load — a direct stage boot loaded raw big-endian ROM before.
|
||||
3. **D88.5** — stan tile-name byte-swap in the converter (0/276 →
|
||||
273/273 name matches during pad setup).
|
||||
4. **D88.6** — intro CAMERA `lang1c` is a `u16` pair, not an `s32`
|
||||
(fixed the `langGet` NULL-bank crash).
|
||||
5. **D89** — `init_path_table_links` `[-3]` OOB → SIGILL fix; NULL-tile
|
||||
guard in `sub_GAME_7F0B0914`.
|
||||
6. **D90** — `stanTileDistanceRelated`'s 80-byte zero-fill was clobbering
|
||||
the caller's live stan-tile local (16B struct, 80B clear). The
|
||||
player's spawn stan was fine all along.
|
||||
7. **D91** — `(s32)&D_800442FC[portalnum]` truncation in bg portal cull.
|
||||
8. **D92** — `chrAllocate` `s32` param truncated the ailist pointer;
|
||||
`Model.unka0` `s32` field truncated a stored function pointer.
|
||||
|
||||
**Next steps for the resuming session (render milestone):**
|
||||
1. **D85** — root cause confirmed (8-byte N64 `Gfx` vs 16-byte PC `Gfx`
|
||||
in the unconverted per-room DL blob). Runtime widen+bswap fixup being
|
||||
implemented; verify with `GE_D69BB=1` (real GBI opcodes, not
|
||||
00/01/02/52), then `GE_PCDUMP` + `tools_pc/pixcount.py` for
|
||||
non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED".
|
||||
Fallback if the widen stalls: soften the four `sysFatalError("Bad
|
||||
size…")` guards in `gfx_pc.cpp` to skip-with-warning.
|
||||
2. Then the front-end **render bugs (D75)**.
|
||||
|
||||
**D88.4 loose end (still open):** `PROPDEF_PC_BYTES` for `VEHICHLE`/
|
||||
`AIRCRAFT`/`TANK`/`AMMO`/`DEPOSIT_IN_ROOM` in `d88_propdefs.py` are
|
||||
placeholder guesses (BUNKER1 doesn't use them). Probe real sizes via
|
||||
`d88_layoutprobe.c` before loading levels that use those types.
|
||||
|
||||
## Known rendering bugs (D75 — still open, orthogonal to the D88 crash)
|
||||
|
||||
Even once the crash chain is cleared, the front end has **broken 3D model
|
||||
rendering** (user-confirmed this session):
|
||||
- Rareware logo: correct (fixed in D73/D74).
|
||||
- **Nintendo logo**: renders but **mispositioned**.
|
||||
- **Gun-barrel intro**: the **James Bond character model is missing
|
||||
entirely**.
|
||||
- **Intro credits / cast roll**: the per-character 3D models **do not
|
||||
appear at all** (names draw, models don't).
|
||||
Pattern: textures/text draw; **animated/skeletal character models never
|
||||
appear**; static 3D (logos) appears but with a bad transform. Leading
|
||||
hypothesis is D75(b) — the animated-model path (`animInit` + raw offsets
|
||||
into `struct player`, cf. D56) is broken independently of the D73 matrix
|
||||
sin/cos fix. Full triage plan in `findings.md` §F D75.
|
||||
|
||||
The rest of this document (below) is the **last known-good, committed**
|
||||
status as of commit `8c9c6a2c` (D86+D87 resolved) — still accurate except
|
||||
D88 is now further along (D88.1–3 done/verified, D88.4 is the live crash).
|
||||
|
||||
## Where things stand
|
||||
|
||||
**D69 (the original "stage load faults" milestone blocker) remains
|
||||
RESOLVED.** `load_bg_file` (bg.c) doesn't fault on BUNKER1. Since then,
|
||||
two more crashes further down the load chain were found and fixed this
|
||||
session (D86, D87), and a third — the current blocker — was root-caused
|
||||
but not fixed (D88).
|
||||
|
||||
**D86 RESOLVED.** `modelInitRwData` crash (`model.c:6174`) was a single
|
||||
truncating pointer cast in the player's embedded gait/arm model:
|
||||
`src/game/initplayergaitobject.c:5` did
|
||||
`player_gait_object_header.RootNode = (int)&player_gait_hdr;` — a
|
||||
same-width no-op on N64 that truncates+zero-extends a real 64-bit pointer
|
||||
on PC. Fixed with a narrow `#ifdef PORT` branch assigning the pointer
|
||||
directly. Root-caused via a new node-walk trace (`GE_D86=1`, left in
|
||||
place, gated in `model.c`/`objecthandler_2.c`).
|
||||
|
||||
**D87 RESOLVED.** Once D86 stopped blocking progress, an idle (no-input)
|
||||
run eventually triggers the front-end's genuine attract-mode demo
|
||||
playback (`select_ramrom_to_play()` picks a random compiled-in demo —
|
||||
this is shipped retail behavior, not a debug feature) and crashed in
|
||||
`ramrom_replay_handler` (`ramromreplay.c`). Root cause: `ramromfilestructure`
|
||||
is a real ROM-compiled asset (big-endian, like everything else) loaded via
|
||||
`romCopyAligned()` — a raw byte copy by design (D66) — with **no
|
||||
byteswap**, so every multi-byte field read back scrambled (e.g. `size_cmds`
|
||||
2 → 33554432) and drove wild pointer arithmetic. Fixed with a `#ifdef PORT`
|
||||
`ramromFixupEndian()` called once after the load (same pattern as the D54
|
||||
cseq fixup). Not BUNKER1-specific — attract mode picks any of 7 demo
|
||||
locations at random, so don't rely on it for BUNKER1-specific testing (see
|
||||
`-level_09` below).
|
||||
|
||||
**D88 — SUPERSEDED, see "READ THIS FIRST" at top.** D88.1–D88.3 (header +
|
||||
sub-table width/endian conversion) are now done and verified; D88.4
|
||||
(`propDefs` byteswap) is the live blocker. The paragraph below is the
|
||||
original root-cause writeup, kept for context.
|
||||
|
||||
**D88 (original writeup) — root-caused.** Launch with
|
||||
`-level_09` (NTSC `LEVELID_BUNKER1 = 9`; `boss.c:199-339` decodes
|
||||
`-level_XX` into `g_StageNum`, bypassing the front end/attract-mode
|
||||
entirely — fast, deterministic BUNKER1 repro, crashes in well under a
|
||||
minute instead of waiting ~2 min for attract mode to maybe pick Bunker).
|
||||
Crash: `proplvreset2` (`prop.c:1306`) segfaults reading
|
||||
`g_CurrentSetup.pathwaypoints[i1].padID`. Root cause: the per-level
|
||||
`"Usetup<name>Z"` file (`prop.c:1267`, `struct stagesetup` in
|
||||
`bondtypes.h:4091`) is loaded as raw ROM bytes and has **zero PC porting
|
||||
work done on it** — unlike bg/stan (D69/D80-82) and models (D43/D50).
|
||||
Two compounding problems, not just one:
|
||||
1. The 10 top-level fields (`pathwaypoints`/`waypointgroups`/`intro`/
|
||||
`propDefs`/`patrolpaths`/`ailists`/`pads`/`boundpads`/`padnames`/
|
||||
`boundpadnames`) are declared as real pointers in the live C struct,
|
||||
so on PC they're 8 bytes each (an 80-byte header) instead of the
|
||||
file's real 4-byte-each (40-byte) N64 layout — same class as D79
|
||||
(`bg_room_data` pointer growth). Field 0 reads fine; everything after
|
||||
it is reading the wrong bytes entirely.
|
||||
2. The 4 meaningful bytes each field *does* store are big-endian (the
|
||||
code's own comment: "stores every internal reference as a byte offset
|
||||
from the start of the file") and nothing byte-swaps them — same class
|
||||
as D87.
|
||||
No PORT/byteswap handling exists anywhere in `prop.c` (confirmed by grep).
|
||||
**Not fixed this session** — this is D69-scale format-conversion work: a
|
||||
byte-accurate spec of the whole `Usetup*Z` format (top-level header +
|
||||
every nested sub-table: `waypoint`/`waygroup`/`PropDefHeaderRecord`/
|
||||
`PathRecord`/`AIListRecord`/`PadRecord`/`BoundPadRecord`/`pname`, each
|
||||
likely with its own internal offsets not yet audited) plus either an
|
||||
offline converter sidecar (preferred pattern per AGENTS.md, same shape as
|
||||
`tools_pc/d69_emit.py`) or a careful runtime fixup pass that parses the
|
||||
raw 40-byte N64-packed header by explicit byte offset, byte-swaps each
|
||||
field, and writes results into the PC-widened struct.
|
||||
|
||||
**Net effect vs. last session:** the game now runs substantially further
|
||||
— all the way through room-streaming setup, past the intro's model
|
||||
pipeline, and into per-level "Usetup" data — before hitting D88. The
|
||||
"loads without fault" acceptance bar is **still not met**, but the
|
||||
remaining blocker is now narrowly scoped and has a fast, deterministic
|
||||
repro (`-level_09`, no attract-mode wait, no depending on which random
|
||||
demo attract-mode picks).
|
||||
|
||||
## Recommended next steps, in order
|
||||
|
||||
1. **D88.** Get a byte-level spec of `Usetup*Z` (start from BUNKER1's
|
||||
file; `strResource` is built as `"U" + "sev" + "Z"`-style name in
|
||||
`prop.c:1253-1265` — check `setup_text_pointers[LEVELID_BUNKER1]` for
|
||||
the exact literal). Decide offline-converter vs. runtime-fixup (D69's
|
||||
bg/stan work is the template for the former; D54's cseq fixup is the
|
||||
template for the latter — given the struct-width mismatch on top of
|
||||
the byteswap, a runtime fixup that manually walks the *raw* 40-byte
|
||||
N64 header by hand (not through the live `stagesetup` struct) into a
|
||||
freshly-populated `g_CurrentSetup` is probably simpler than a full
|
||||
sidecar here, but verify against a raw ROM hex dump either way).
|
||||
2. Once BUNKER1 loads past `proplvreset2`, re-check for further crashes
|
||||
in the same vein (this session found 3 in a row — D86, D87, D88 — each
|
||||
only reachable after the previous one was fixed; expect more).
|
||||
3. Once BUNKER1 reaches a rendered frame with no fault, revisit **D85**
|
||||
(room primary/secondary GDL binaries decode to garbage via
|
||||
`texLoadFromGdl`) — use `GE_PCDUMP="<range>:10"` + `tools_pc/pixcount.py`
|
||||
to confirm non-black, non-degenerate content per the original D69
|
||||
acceptance bar. (This session's `GE_PCDUMP` captures around frame
|
||||
2100-2400 during attract-mode-driven "loading" were still on a HUD/menu
|
||||
screen, not real 3D geometry — don't read too much into pixel counts
|
||||
from before D88 is fixed.)
|
||||
4. Do NOT touch D75/D76/D77 (parked, lower priority, unrelated).
|
||||
|
||||
## Debug tooling added this session (kept, env-gated, zero cost when unset)
|
||||
|
||||
- `GE_D86=1` — node-walk trace in `modelInitRwData` (model.c) + a
|
||||
load-identity probe in `load_object_fill_header` (objecthandler_2.c).
|
||||
Resolved the D86 crash; left in place since the same
|
||||
load_object_fill_header/modelInitRwData pipeline could surface new
|
||||
edge cases as more of the game becomes reachable.
|
||||
- `GE_D87=1` — block-setup trace in `iterate_ramrom_entries_handle_camera_out`
|
||||
and consumer trace in `ramrom_replay_handler` (ramromreplay.c). Resolved
|
||||
the D87 crash; left in place as it's a rarely-exercised path (attract
|
||||
mode) worth having visibility into if it acts up again.
|
||||
- Pre-existing `GE_D69STAN=1`, `GE_D69BB=1`, `GE_D69=1` — unchanged, still
|
||||
useful for the bg/stan/D85 load path (see prior session's HANDOFF
|
||||
entries, preserved in git history, for exactly what each logs).
|
||||
|
||||
## New: fast, deterministic BUNKER1 repro (no attract-mode wait)
|
||||
|
||||
Launch with `-level_09` as a program argument
|
||||
(`./build-pc/ge007.x86_64.exe -level_09`) to skip the front end/attract
|
||||
mode and load BUNKER1 directly — `boss.c:199-339` decodes `-level_XX`
|
||||
(the two digit-chars are consumed as raw ASCII bytes:
|
||||
`g_StageNum = tokenFindLevel[0]*10 + tokenFindLevel[1] - 0x210`; NTSC
|
||||
`LEVELID_BUNKER1 = 9` → `"09"` since `'0'*10 + '9' - 0x210 = 9`). This is
|
||||
now the preferred way to test BUNKER1-specific load/render work — it's
|
||||
faster (crashes/completes in well under a minute vs. ~2+ min waiting on
|
||||
attract mode) and deterministic (not dependent on which of 7 random demo
|
||||
locations attract mode happens to pick).
|
||||
|
||||
## Environment / build
|
||||
|
||||
- `export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final`
|
||||
(~5 s). Build is GREEN with all D86/D87 work included.
|
||||
- Run from the **repo root**, not `build-pc/`.
|
||||
- Regenerate sidecars if missing: `python tools_pc/d69_emit.py ntsc-final`
|
||||
(bg/stan) and the D43/D50 model sidecar generator (pcmodels) — both
|
||||
gitignored, not checked in. (Both were already present in this dev
|
||||
environment this session.)
|
||||
- `GE_PCDUMP="<start>-<end>:<stride>"` + `tools_pc/pixcount.py` for frame
|
||||
captures once D88 is fixed and a frame actually renders real BUNKER1
|
||||
geometry.
|
||||
- Crash log: `ge007.crash.log` (repo root); symbolicate with
|
||||
`addr2line -e build-pc/ge007.x86_64.exe -f -C <pc>` (image base
|
||||
`0x140000000`).
|
||||
- gdb **launch** mode is too slow for timing-dependent bugs (unchanged
|
||||
guidance). **New this session:** gdb **attach** mode
|
||||
(`gdb -batch -x cmds.txt -p <winpid>`, `<winpid>` = 4th column of
|
||||
`ps -p <bashpid>`) works well and is fast for "watch a global for a
|
||||
legitimate vs. corrupted write" questions on an already-running,
|
||||
not-yet-crashed process — see `docs/internals.md` §F environment
|
||||
reminders for the exact recipe used to root-cause D87.
|
||||
|
||||
## Non-negotiables (unchanged, see AGENTS.md)
|
||||
|
||||
1. N64 build files untouched.
|
||||
2. Game logic unmodified except narrow, documented `#ifdef PORT`
|
||||
ABI/layout exceptions (D86/D87 this session, both logged in
|
||||
`findings.md` §F). D88 is explicitly **not** patched with a
|
||||
quick inline hack — it needs the same disciplined
|
||||
spec-then-convert/fixup treatment as D69, logged as an open finding
|
||||
instead per AGENTS.md's "stop and write it up" guidance for anything
|
||||
beyond a narrow, obviously-correct exception.
|
||||
3. Offline sidecar conversion preferred over runtime fixup for whole
|
||||
ROM-asset formats (D69/D80-82 pattern) — likely the right call for
|
||||
D88 too, though a careful runtime fixup is also plausible; decide
|
||||
after the byte-level spec work.
|
||||
@@ -0,0 +1,362 @@
|
||||
# LEVEL-OBJECTIVES — per-level mission objectives + win/fail criteria
|
||||
|
||||
Auto-generated by `tools_pc/dump_objectives.py` from the retail ROM (M-30, Track A).
|
||||
Criteria semantics: see the header of `tools_pc/dump_objectives.py`.
|
||||
'min difficulty' = the lowest difficulty at which the objective is REQUIRED
|
||||
(D157: the reader was fixed 2026-08-31 — earlier revs showed 'Agent' for all).
|
||||
|
||||
|
||||
=== level_09 (level 9, UsetupsevbunkerZ) ===
|
||||
Objective 0 [briefing text id 30749] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 0 -> CCTV
|
||||
- DESTROY_OBJECT tag 1 -> CCTV
|
||||
- DESTROY_OBJECT tag 2 -> CCTV
|
||||
- DESTROY_OBJECT tag 3 -> CCTV
|
||||
Objective 1 [briefing text id 30750] min difficulty: Agent
|
||||
- COPY_ITEM (key-analyzer/datavac copy flag)
|
||||
Objective 2 [briefing text id 30751] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 30752] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 4 [briefing text id 30753] min difficulty: Agent
|
||||
- PHOTOGRAPH tag 5 -> MULTI_MONITOR; photo-flag word @8=0x0
|
||||
|
||||
=== level_20 (level 20, UsetupsiloZ) ===
|
||||
Objective 0 [briefing text id 34834] min difficulty: No One Lives Forever
|
||||
- DEPOSIT_IN_ROOM ref @4=34 ref @8=4; runtime word @c=0x0
|
||||
- DEPOSIT_IN_ROOM ref @4=34 ref @8=12; runtime word @c=0x0
|
||||
- DEPOSIT_IN_ROOM ref @4=34 ref @8=88; runtime word @c=0x0
|
||||
- DEPOSIT_IN_ROOM ref @4=34 ref @8=95; runtime word @c=0x0
|
||||
Objective 1 [briefing text id 34835] min difficulty: Agent
|
||||
- PHOTOGRAPH tag 0 -> PROP; photo-flag word @8=0x0
|
||||
Objective 2 [briefing text id 34836] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 1 -> PROP
|
||||
Objective 3 [briefing text id 34837] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 2 -> PROP
|
||||
- COLLECT_OBJECT tag 3 -> PROP
|
||||
- COLLECT_OBJECT tag 4 -> PROP
|
||||
- COLLECT_OBJECT tag 5 -> PROP
|
||||
- FAIL_CONDITION stage flag mask 0x100 (bit(s) [8]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x400 (bit(s) [10]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
Objective 4 [briefing text id 34838] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
- FAIL_CONDITION stage flag mask 0x4000 (bit(s) [14]) -- mission FAILS when set
|
||||
|
||||
=== level_21 (level 21, UsetupsevbunkerZ) ===
|
||||
Objective 0 [briefing text id 30749] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 0 -> CCTV
|
||||
- DESTROY_OBJECT tag 1 -> CCTV
|
||||
- DESTROY_OBJECT tag 2 -> CCTV
|
||||
- DESTROY_OBJECT tag 3 -> CCTV
|
||||
Objective 1 [briefing text id 30750] min difficulty: Agent
|
||||
- COPY_ITEM (key-analyzer/datavac copy flag)
|
||||
Objective 2 [briefing text id 30751] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 30752] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 4 [briefing text id 30753] min difficulty: Agent
|
||||
- PHOTOGRAPH tag 5 -> MULTI_MONITOR; photo-flag word @8=0x0
|
||||
|
||||
=== level_22 (level 22, UsetupstatueZ) ===
|
||||
Objective 0 [briefing text id 35897] min difficulty: Agent
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
Objective 1 [briefing text id 35898] min difficulty: Agent
|
||||
- FAIL_CONDITION stage flag mask 0x20000 (bit(s) [17]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
Objective 2 [briefing text id 35899] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x200 (bit(s) [9])
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 35900] min difficulty: Agent
|
||||
- FAIL_CONDITION stage flag mask 0x400000 (bit(s) [22]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
- COMPLETE_CONDITION stage flag mask 0x800 (bit(s) [11])
|
||||
Objective 4 [briefing text id 35901] min difficulty: Agent
|
||||
- COLLECT_OBJECT tag 1 -> PROP
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
|
||||
=== level_23 (level 23, UsetupcontrolZ) ===
|
||||
Objective 0 [briefing text id 8233] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x1000 (bit(s) [12])
|
||||
- FAIL_CONDITION stage flag mask 0x2000 (bit(s) [13]) -- mission FAILS when set
|
||||
Objective 1 [briefing text id 8234] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x4000 (bit(s) [14])
|
||||
- FAIL_CONDITION stage flag mask 0x10000 (bit(s) [16]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 8235] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 21 -> PROP
|
||||
- DESTROY_OBJECT tag 22 -> PROP
|
||||
- DESTROY_OBJECT tag 23 -> PROP
|
||||
- DESTROY_OBJECT tag 24 -> PROP
|
||||
- DESTROY_OBJECT tag 25 -> PROP
|
||||
- DESTROY_OBJECT tag 26 -> PROP
|
||||
|
||||
=== level_24 (level 24, UsetuparchZ) ===
|
||||
Objective 0 [briefing text id 2089] min difficulty: Agent
|
||||
- ENTER_ROOM ref @4=152 (room); runtime word @8=0x0
|
||||
Objective 1 [briefing text id 2090] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
Objective 2 [briefing text id 2091] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 2 -> PROP
|
||||
- FAIL_CONDITION stage flag mask 0x1000 (bit(s) [12]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 2092] min difficulty: Agent
|
||||
- ENTER_ROOM ref @4=208 (room); runtime word @8=0x0
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
|
||||
=== level_25 (level 25, UsetuptraZ) ===
|
||||
Objective 0 [briefing text id 36898] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 8 -> PROP
|
||||
- DESTROY_OBJECT tag 9 -> PROP
|
||||
- DESTROY_OBJECT tag 10 -> PROP
|
||||
- DESTROY_OBJECT tag 11 -> PROP
|
||||
- DESTROY_OBJECT tag 12 -> PROP
|
||||
- DESTROY_OBJECT tag 13 -> PROP
|
||||
Objective 1 [briefing text id 36899] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
- FAIL_CONDITION stage flag mask 0x4000 (bit(s) [14]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 36900] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x10000 (bit(s) [16])
|
||||
Objective 3 [briefing text id 36901] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x20000 (bit(s) [17])
|
||||
Objective 4 [briefing text id 36902] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100000 (bit(s) [20])
|
||||
|
||||
=== level_26 (level 26, UsetupdestZ) ===
|
||||
Objective 0 [briefing text id 13332] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- COMPLETE_CONDITION stage flag mask 0x200 (bit(s) [9])
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
- COMPLETE_CONDITION stage flag mask 0x800 (bit(s) [11])
|
||||
- COMPLETE_CONDITION stage flag mask 0x1000 (bit(s) [12])
|
||||
- FAIL_CONDITION stage flag mask 0x100000 (bit(s) [20]) -- mission FAILS when set
|
||||
Objective 1 [briefing text id 13333] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x10000 (bit(s) [16])
|
||||
- FAIL_CONDITION stage flag mask 0x40000 (bit(s) [18]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 13334] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x20000 (bit(s) [17])
|
||||
- FAIL_CONDITION stage flag mask 0x80000 (bit(s) [19]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 13335] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x4000 (bit(s) [14])
|
||||
- FAIL_CONDITION stage flag mask 0x8000 (bit(s) [15]) -- mission FAILS when set
|
||||
|
||||
=== level_27 (level 27, UsetupsevbZ) ===
|
||||
Objective 0 [briefing text id 29743] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x200 (bit(s) [9])
|
||||
Objective 1 [briefing text id 29744] min difficulty: Agent
|
||||
- FAIL_CONDITION stage flag mask 0x1000 (bit(s) [12]) -- mission FAILS when set
|
||||
- COLLECT_OBJECT tag 2 -> PROP
|
||||
Objective 2 [briefing text id 29745] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 28 -> CCTV
|
||||
- DESTROY_OBJECT tag 29 -> CCTV
|
||||
- DESTROY_OBJECT tag 30 -> CCTV
|
||||
- DESTROY_OBJECT tag 31 -> CCTV
|
||||
- DESTROY_OBJECT tag 32 -> CCTV
|
||||
- DESTROY_OBJECT tag 33 -> CCTV
|
||||
Objective 3 [briefing text id 29746] min difficulty: No One Lives Forever
|
||||
- COLLECT_OBJECT tag 34 -> PROP
|
||||
Objective 4 [briefing text id 29747] min difficulty: Agent
|
||||
- FAIL_CONDITION stage flag mask 0x100 (bit(s) [8]) -- mission FAILS when set
|
||||
- COMPLETE_CONDITION stage flag mask 0x800 (bit(s) [11])
|
||||
- ENTER_ROOM ref @4=51 (room); runtime word @8=0x0
|
||||
|
||||
=== level_28 (level 28, UsetupaztZ) ===
|
||||
Objective 0 [briefing text id 5141] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x1000 (bit(s) [12])
|
||||
Objective 1 [briefing text id 5142] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
|
||||
=== level_29 (level 29, UsetuppeteZ) ===
|
||||
Objective 0 [briefing text id 25627] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x20000 (bit(s) [17])
|
||||
- FAIL_CONDITION stage flag mask 0x40000 (bit(s) [18]) -- mission FAILS when set
|
||||
Objective 1 [briefing text id 25625] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x4000 (bit(s) [14])
|
||||
- FAIL_CONDITION stage flag mask 0x8000 (bit(s) [15]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 25626] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x4000 (bit(s) [14])
|
||||
- FAIL_CONDITION stage flag mask 0x1000 (bit(s) [12]) -- mission FAILS when set
|
||||
|
||||
=== level_30 (level 30, UsetupdepoZ) ===
|
||||
Objective 0 [briefing text id 12303] min difficulty: No One Lives Forever
|
||||
- DESTROY_OBJECT tag 10 -> PROP
|
||||
- DESTROY_OBJECT tag 11 -> PROP
|
||||
- DESTROY_OBJECT tag 12 -> PROP
|
||||
- DESTROY_OBJECT tag 13 -> PROP
|
||||
- DESTROY_OBJECT tag 14 -> PROP
|
||||
- DESTROY_OBJECT tag 15 -> PROP
|
||||
- DESTROY_OBJECT tag 16 -> PROP
|
||||
- DESTROY_OBJECT tag 17 -> PROP
|
||||
- DESTROY_OBJECT tag 18 -> PROP
|
||||
- DESTROY_OBJECT tag 19 -> PROP
|
||||
- DESTROY_OBJECT tag 20 -> PROP
|
||||
- DESTROY_OBJECT tag 21 -> PROP
|
||||
- DESTROY_OBJECT tag 22 -> PROP
|
||||
- DESTROY_OBJECT tag 23 -> PROP
|
||||
- DESTROY_OBJECT tag 24 -> PROP
|
||||
- DESTROY_OBJECT tag 25 -> PROP
|
||||
- DESTROY_OBJECT tag 26 -> PROP
|
||||
Objective 1 [briefing text id 12304] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 1 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 2 -> PROP
|
||||
- DESTROY_OBJECT tag 3 -> PROP
|
||||
Objective 2 [briefing text id 12305] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 0 -> KEY
|
||||
Objective 3 [briefing text id 12306] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 5 -> PROP
|
||||
Objective 4 [briefing text id 12307] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
UsetuprefZ not in filelist
|
||||
|
||||
=== dam (level 33, UsetupdamZ) ===
|
||||
Objective 0 [briefing text id 11286] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 0 -> ALARM
|
||||
- DESTROY_OBJECT tag 1 -> ALARM
|
||||
- DESTROY_OBJECT tag 2 -> ALARM
|
||||
- DESTROY_OBJECT tag 3 -> ALARM
|
||||
Objective 1 [briefing text id 11287] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x2000 (bit(s) [13]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 11288] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x400 (bit(s) [10])
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 11289] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x1000 (bit(s) [12])
|
||||
|
||||
=== facility (level 34, UsetuparkZ) ===
|
||||
Objective 0 [briefing text id 3120] min difficulty: Agent
|
||||
- ENTER_ROOM ref @4=86 (room); runtime word @8=0x0
|
||||
Objective 1 [briefing text id 3121] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x200 (bit(s) [9])
|
||||
- FAIL_CONDITION stage flag mask 0x10000 (bit(s) [16]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 3122] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x1000 (bit(s) [12]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 3123] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 1 -> GAS
|
||||
- DESTROY_OBJECT tag 2 -> GAS
|
||||
- DESTROY_OBJECT tag 3 -> GAS
|
||||
- DESTROY_OBJECT tag 4 -> GAS
|
||||
- DESTROY_OBJECT tag 5 -> GAS
|
||||
- DESTROY_OBJECT tag 6 -> GAS
|
||||
- DESTROY_OBJECT tag 7 -> GAS
|
||||
- DESTROY_OBJECT tag 8 -> GAS
|
||||
- DESTROY_OBJECT tag 9 -> GAS
|
||||
- DESTROY_OBJECT tag 10 -> GAS
|
||||
Objective 4 [briefing text id 3124] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
- FAIL_CONDITION stage flag mask 0x4000 (bit(s) [14]) -- mission FAILS when set
|
||||
|
||||
=== runway (level 35, UsetuprunZ) ===
|
||||
Objective 0 [briefing text id 28687] min difficulty: Agent
|
||||
- COLLECT_OBJECT tag 2 -> KEY
|
||||
Objective 1 [briefing text id 28688] min difficulty: No One Lives Forever
|
||||
- DESTROY_OBJECT tag 0 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 4 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 5 -> AUTOGUN
|
||||
Objective 2 [briefing text id 28689] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 1 -> PROP
|
||||
Objective 3 [briefing text id 28690] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x800 (bit(s) [11])
|
||||
- FAIL_CONDITION stage flag mask 0x400 (bit(s) [10]) -- mission FAILS when set
|
||||
|
||||
=== surface1 (level 36, UsetupsevxZ) ===
|
||||
Objective 0 [briefing text id 31760] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
Objective 1 [briefing text id 31761] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 0 -> KEY
|
||||
Objective 2 [briefing text id 31762] min difficulty: Secret Agent
|
||||
- COLLECT_OBJECT tag 2 -> PROP
|
||||
Objective 3 [briefing text id 31763] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
|
||||
=== surface2 (level 43, UsetupsevxbZ) ===
|
||||
Objective 0 [briefing text id 32788] min difficulty: No One Lives Forever
|
||||
- DESTROY_OBJECT tag 3 -> CCTV
|
||||
- DESTROY_OBJECT tag 4 -> CCTV
|
||||
- DESTROY_OBJECT tag 6 -> CCTV
|
||||
- DESTROY_OBJECT tag 8 -> CCTV
|
||||
Objective 1 [briefing text id 32789] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 0 -> PROP
|
||||
- DESTROY_OBJECT tag 1 -> PROP
|
||||
- FAIL_CONDITION stage flag mask 0x400000 (bit(s) [22]) -- mission FAILS when set
|
||||
Objective 2 [briefing text id 32790] min difficulty: Secret Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
- FAIL_CONDITION stage flag mask 0x200 (bit(s) [9]) -- mission FAILS when set
|
||||
Objective 3 [briefing text id 32791] min difficulty: Agent
|
||||
- ENTER_ROOM ref @4=3 (room); runtime word @8=0x0
|
||||
|
||||
=== jungle (level 37, UsetupjunZ) ===
|
||||
Objective 0 [briefing text id 18469] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 0 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 1 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 2 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 9 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 10 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 11 -> AUTOGUN
|
||||
- DESTROY_OBJECT tag 12 -> AUTOGUN
|
||||
Objective 1 [briefing text id 18470] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
Objective 2 [briefing text id 18471] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 20 -> PROP
|
||||
- DESTROY_OBJECT tag 21 -> PROP
|
||||
- DESTROY_OBJECT tag 22 -> PROP
|
||||
- DESTROY_OBJECT tag 23 -> PROP
|
||||
- DESTROY_OBJECT tag 24 -> PROP
|
||||
- DESTROY_OBJECT tag 25 -> PROP
|
||||
- DESTROY_OBJECT tag 26 -> PROP
|
||||
- DESTROY_OBJECT tag 27 -> PROP
|
||||
- DESTROY_OBJECT tag 28 -> PROP
|
||||
Objective 3 [briefing text id 18472] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x8000 (bit(s) [15])
|
||||
- FAIL_CONDITION stage flag mask 0x400 (bit(s) [10]) -- mission FAILS when set
|
||||
|
||||
=== caverns (level 39, UsetupcaveZ) ===
|
||||
Objective 0 [briefing text id 7187] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 0 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 1 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 2 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 3 -> MULTI_MONITOR
|
||||
Objective 1 [briefing text id 7188] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 4 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 5 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 6 -> MULTI_MONITOR
|
||||
- DESTROY_OBJECT tag 7 -> MULTI_MONITOR
|
||||
Objective 2 [briefing text id 7189] min difficulty: Secret Agent
|
||||
- DESTROY_OBJECT tag 8 -> MULTI_MONITOR
|
||||
Objective 3 [briefing text id 7190] min difficulty: No One Lives Forever
|
||||
- COMPLETE_CONDITION stage flag mask 0x400000 (bit(s) [22])
|
||||
- FAIL_CONDITION stage flag mask 0x40000 (bit(s) [18]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x80000 (bit(s) [19]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x100000 (bit(s) [20]) -- mission FAILS when set
|
||||
- FAIL_CONDITION stage flag mask 0x200000 (bit(s) [21]) -- mission FAILS when set
|
||||
Objective 4 [briefing text id 7192] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x800000 (bit(s) [23])
|
||||
- FAIL_CONDITION stage flag mask 0x1000000 (bit(s) [24]) -- mission FAILS when set
|
||||
|
||||
=== cradle (level 41, UsetupcradZ) ===
|
||||
Objective 0 [briefing text id 9247] min difficulty: Agent
|
||||
- DESTROY_OBJECT tag 0 -> MONITOR
|
||||
- FAIL_CONDITION stage flag mask 0x800 (bit(s) [11]) -- mission FAILS when set
|
||||
Objective 1 [briefing text id 9248] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x100 (bit(s) [8])
|
||||
|
||||
=== egypt (level 32, UsetupcrypZ) ===
|
||||
Objective 0 [briefing text id 10252] min difficulty: Agent
|
||||
- COLLECT_OBJECT tag 4 -> COLLECTABLE
|
||||
- COLLECT_OBJECT tag 5 -> AMMO
|
||||
Objective 1 [briefing text id 10253] min difficulty: Agent
|
||||
- COMPLETE_CONDITION stage flag mask 0x4000 (bit(s) [14])
|
||||
|
||||
=== cuba (level 54, UsetuplenZ) ===
|
||||
(no objective records)
|
||||
@@ -0,0 +1,77 @@
|
||||
# LEVEL-PLAYTEST — WS6 human completion-validation checklist
|
||||
|
||||
Run **after** `docs/LEVEL-STATUS.md` is all-21 load+render PASS. This is
|
||||
the part that needs real input — a human plays each level start to finish
|
||||
and confirms it matches retail. Findings feed a WS5-style triage round
|
||||
(log each as a crash-class row or a new Dxx candidate).
|
||||
|
||||
## How to run one level
|
||||
|
||||
```sh
|
||||
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final
|
||||
./build-pc/ge007.x86_64.exe -level_XX # bare — pools auto-injected (D121)
|
||||
```
|
||||
|
||||
Controls: `port/src/input.c` — keyboard+mouse or SDL gamepad. Mouse-look is
|
||||
mode-aware since M-24 (hold RMB = analog aim). **M-24 input/QoL changes +
|
||||
what to verify while playing: `docs/M-24-QOL-REVIEW.md`.** Tune feel in
|
||||
`data/ge007.ini` `[Input]` (`MouseAimSpeed` 50, `MouseTurnSpeed` 100,
|
||||
`MouseInvertY` 0) with the game closed. Known residual (do not re-log):
|
||||
D118a — hipfire pitch is digital vs analog yaw. **F12** = screenshot to
|
||||
`ppm/shot_NNN.ppm`. **Mouse wheel** = cycle weapon. **Alt-Enter** =
|
||||
fullscreen. Alt-tab away frees the cursor (re-grabs on focus). Title bar
|
||||
shows FPS.
|
||||
Difficulty for a bare `-level_XX` boot defaults to Agent; use the menu path
|
||||
(mission select → difficulty) to test 00/007.
|
||||
|
||||
Objective status: `src/game/objective_status.c`
|
||||
(`get_status_of_objective()` `:161`); objective propDefs are
|
||||
`PROPDEF_OBJECTIVE_*` (`bondconstants.h:4332`).
|
||||
|
||||
## Per-level checklist (copy this block per level)
|
||||
|
||||
```
|
||||
### <Level> (-level_XX) difficulty tested: [ ] Agent [ ] Secret Agent [ ] 00 Agent
|
||||
|
||||
- [ ] Spawn point + start orientation match retail
|
||||
- [ ] Geometry / textures / lighting recognisable, no missing rooms
|
||||
- [ ] Guards spawn, path, react, take/deal damage, die correctly
|
||||
- [ ] Doors / lifts / switches / destructibles work
|
||||
- [ ] Pickups (ammo, armour, key items, gadgets) present and collectable
|
||||
- [ ] Each difficulty-gated objective reachable AND registers COMPLETE
|
||||
(objective_status)
|
||||
- [ ] Alarms / reinforcements / scripted events fire
|
||||
- [ ] Level exit trigger fires → MISSION COMPLETE (not a hang / wrong screen)
|
||||
- [ ] Auto-advance to next briefing works
|
||||
- [ ] Frame rate + timing acceptable
|
||||
- Notes / defects:
|
||||
```
|
||||
|
||||
## 21 solo levels (mission order — numbers = `-level_XX`)
|
||||
|
||||
| # | Level | -level | Briefing asset |
|
||||
|---|---|---|---|
|
||||
| 1 | Dam | 33 | UbriefdamZ |
|
||||
| 2 | Facility | 34 | UbriefarkZ |
|
||||
| 3 | Runway | 35 | UbriefrunZ |
|
||||
| 4 | Surface 1 | 36 | UbriefsevxZ |
|
||||
| 5 | Bunker 1 | 09 | UbriefsevbunkerZ |
|
||||
| 6 | Silo | 20 | UbriefsiloZ |
|
||||
| 7 | Frigate | 26 | UbriefdestZ |
|
||||
| 8 | Surface 2 | 43 | UbriefsevxbZ |
|
||||
| 9 | Bunker 2 | 27 | UbriefsevbZ |
|
||||
| 10 | Statue | 22 | UbriefstatueZ |
|
||||
| 11 | Archives | 24 | UbriefarchZ |
|
||||
| 12 | Streets | 29 | UbriefpeteZ |
|
||||
| 13 | Depot | 30 | UbriefdepoZ |
|
||||
| 14 | Train | 25 | UbrieftraZ |
|
||||
| 15 | Jungle | 37 | UbriefjunZ |
|
||||
| 16 | Control | 23 | UbriefcontrolZ |
|
||||
| 17 | Caverns | 39 | UbriefcaveZ |
|
||||
| 18 | Cradle | 41 | UbriefcradZ |
|
||||
| 19 | Aztec | 28 | UbriefaztZ |
|
||||
| 20 | Egypt | 32 | UbriefcrypZ |
|
||||
| 21 | Cuba | 54 | (post-Egypt unlock) |
|
||||
|
||||
(Mission table: `front.c:433`. Cuba = `LEVELID_CUBA`, not in the standard
|
||||
folder list — unlocked after Egypt.)
|
||||
@@ -0,0 +1,268 @@
|
||||
# LEVEL-STATUS — 21 solo-level load+render sweep (WS4)
|
||||
|
||||
Method: bare `./build-pc/ge007.x86_64.exe -level_XX` (per-level `-m*` pools
|
||||
auto-injected, D121), `GE_PCDUMP="80-260:40"`, ~24 s watchdog then
|
||||
`taskkill //F //IM ge007.x86_64.exe`, `ge007.crash.log` symbolicated with
|
||||
`addr2line -e build-pc/ge007.x86_64.exe -f -C <PC>` (image base
|
||||
0x140000000). "render %" = `tools_pc/pixcount.py` non-clear on the last
|
||||
captured frame. Binary at `f2beae4b` + this session's build.
|
||||
|
||||
**M-30 re-sweep (2026-08-31, build after D157/D120/RC2): 21 / 21 PASS — no
|
||||
crash logs.** First all-green sweep incl. Cuba (54) — the D129 credits-path
|
||||
crash no longer reproduces on a bare `-level_54` boot (Cuba rendered 92.5%
|
||||
crash-free through the window). Verifies that the M-30 changes — D157
|
||||
(objective difficulty byte-offset), RC2 (`Video.FixMipTextures` mip-upload
|
||||
clip, default on), D120 (`d43_emit.py` PointUsage emit), the `GE_SAVELOG`/
|
||||
`GE_UNLOCK_ALL` `#ifdef PORT` diagnostics, and the front-end mouse-pointer
|
||||
mode — introduced no level-load regression. Machine was idle; `level_sweep.sh`
|
||||
default window/watchdog.
|
||||
|
||||
M-25 re-sweep (2026-08-30, port-QoL build `863f436b`): **20 / 21 PASS**,
|
||||
unchanged. Caverns (39) flagged CRASH `0xc000001d` once under sweep load →
|
||||
2/2 clean on retry (D117/D134 concurrent-load flakiness, not a regression).
|
||||
Cuba (54) still the only real crash (known D129 credits path, parked).
|
||||
**Silo (20):** PASS. A full-length `GE_PCDUMP` capture froze at ~frame 320
|
||||
(silo→Bond camera descent) twice — but both those runs, *and* the pre-M-25
|
||||
baseline comparison, were on a machine still loaded from the sweep. On an
|
||||
idle machine Silo runs fine (user drove it live past ~1800 frames, VI
|
||||
pacemaker healthy, no heartbeat stall). Classic D117/D134 load-sensitive
|
||||
stall, not a deterministic bug and not an M-25 regression. If it recurs on
|
||||
an idle box, count N runs before believing it (PORT-LEARNINGS §E).
|
||||
|
||||
Last full re-sweep: 2026-08-29 (M-22), after D134 landed (SP/DP task-done
|
||||
event no longer dropped into a full sched interruptQ — the "frame-2 hang"
|
||||
that M-13..M-21 wrote off as D117/machine-load flakiness). Build = HEAD
|
||||
`a8f27c16`. Optimized `level_sweep.sh` (early-exit once the GE_PCDUMP set
|
||||
is on disk / STALL_SECS freeze) — full 21-level sweep now ~7 min.
|
||||
|
||||
**M-22 result: 20 / 21 PASS.** Every level loads + renders + survives the
|
||||
capture window EXCEPT Cuba. Facility (34) and Jungle (37) — the M-20
|
||||
re-verify failures — both PASS cleanly now, confirming D134 was their
|
||||
cause. Only remaining crash:
|
||||
|
||||
- **Cuba (54)** — `textMeasure` (`textrelated.c:770`) ← `bondviewRenderCredits`
|
||||
(`bondview2.c:8912`), AV @ `0x1400c3b77`, fault addr NULL, after ~frame
|
||||
900. This is the **known D129 residual**: the bare `-level_54` boot
|
||||
reaches the cast/credits scroll referencing a text bank the real
|
||||
front-end flow would have loaded. Not a real-flow blocker; Cuba loads +
|
||||
renders 300+ frames fine. Parked with D76 (`docs/GRAPHICS-BACKLOG.md`).
|
||||
|
||||
| Level | # | Status | Crash site | Class |
|
||||
|---|---|---|---|---|
|
||||
| Bunker1 | 09 | **PASS** (M-20: re-verified — 1800 frames crash-free, no `ge007.crash.log`, direct `-level_09` boot at ~90us/frame). The M-19 "boot crash `frames=0` PC `0x1400066fc`" did **not** reproduce on `1fc3cff6` in three separate runs → it was sweep flakiness / D117 nondeterminism under machine load, not a real regression. | — |
|
||||
| Silo | 20 | **PASS** 91.7% | — | — |
|
||||
| Archives | 24 | **PASS** 91.1% | — | — |
|
||||
| Train | 25 | **PASS** 87.0% | — | — |
|
||||
| Caverns | 39 | **PASS** 91.6% | — | — |
|
||||
| Egypt | 32 | **PASS** 90.9% | — | — |
|
||||
| Cuba | 54 | **PASS** 92.5% (loads+renders 300+ frames; bare-boot end-credits path `bondviewRenderCredits` still faults intermittently — D76/D129, not a real-flow blocker) | — | — |
|
||||
| Dam | 33 | **PASS** 83.3% | — (C1 fixed, D123) | — |
|
||||
| Frigate | 26 | **PASS** 90.4% | — (C1 fixed, D123) | — |
|
||||
| Statue | 22 | **PASS** 80.1% | — (C1 fixed, D123) | — |
|
||||
| Streets | 29 | **PASS** 91.6% | — (C1 fixed, D123; no C2 crash this run — timing) | — |
|
||||
| Cradle | 41 | **PASS** 55.4% (low — partial render, no crash) | — (C1 fixed, D123) | — |
|
||||
| Runway | 35 | **PASS** | — (D130: `romdataFixupFont` glyph-relayout aliasing) | — |
|
||||
| Facility | 34 | **PASS** (M-20 re-verified on an idle machine: frame 1500+, no crash. The M-20 mid-session "boot crash `frames=0`" was concurrent-load flakiness — three subagents/sessions sharing the box. Fix stands: D130.) | — (D130: `romdataFixupFont` corrupted glyph `#`/`"` pixeldata → fast3d AV) | — |
|
||||
| Jungle | 37 | **PASS** (M-20 re-verified on an idle machine: frame 1500+, no crash. M-19/D131: 2400+ crash-free. The M-20 mid-session "frame-2 hang" was concurrent-load flakiness.) | — (D131: `osVirtualToPhysical()` truncated a compiled `.bss` matrix ptr in `explosionRenderPropSmoke` → `seg_addr` now restores the module high word) | — |
|
||||
| Aztec | 28 | **PASS** 90.8% | — (C3 fixed, D125) | — |
|
||||
| Bunker2 | 27 | **PASS** 91.5% | — (C3r fixed, D126) | — |
|
||||
| Depot | 30 | **PASS** 79.8% | — (C4 fixed, D126) | — |
|
||||
| Control | 23 | **PASS** 90.8% | — (C5 fixed, D128) | — |
|
||||
| Surface2 | 43 | **PASS** 70.4% | — (C6 fixed, D126) | — |
|
||||
| Surface1 | 36 | **PASS** 77.5% | — (C7 guarded, D127) | — |
|
||||
|
||||
**19 / 21** (M-19). D131 cleared C2m Jungle (explosion-DL `G_MTX` truncated
|
||||
compiled-symbol pointer). **But `-level_09` (Bunker1) regressed to a boot
|
||||
crash sometime between M-18 and now** — reproduces on clean master, so it
|
||||
is not an M-19 change; bisect D125→HEAD. Net 20→19 until 09 is refixed.
|
||||
|
||||
**(M-18) 20 / 21 PASS.** D130 cleared C2 Facility + Runway — the crash was
|
||||
`romdataFixupFont` corrupting fontchar glyphs 0/1/2 (in-place 24→32B relayout
|
||||
aliases for low indices), NOT the model-GDL relocation the D124-Facility
|
||||
addendum suspected. Only Jungle (C2m, explosion-DL `G_MTX`, ~frame 300)
|
||||
remains.
|
||||
|
||||
**(older) 18 / 21 PASS.** D123 cleared C1 on all 6. D124 cleared Jungle's texture
|
||||
crash (now hits an explosion-DL matrix crash). D125 cleared C3-Aztec.
|
||||
**D126** (objective sub-record `->next` pointer widens 4→8B → 8-byte store
|
||||
clobbers the next propdef record's header → walk desync → wrong command
|
||||
indices) cleared **C3r Bunker2 + C4 Depot + C6 Surface2** in one fix
|
||||
(`d88_propdefs.py` types 30/32/33/35 → 24B/6w). **D127** guarded a bogus
|
||||
ALSound* in the parked audio path → **C7 Surface1**. **D128** fixed a
|
||||
hardcoded-N64-stride portal-flag write → **C5 Control**. **3 crashes
|
||||
remain, all one class:** C2 Runway + Facility + C2m Jungle — the model /
|
||||
prop GDL relocation misaligns on the 8→16B `Gfx` stride
|
||||
(`docs/BRIEF-C2gdl-model-reloc.md`).
|
||||
|
||||
Notes: Streets passed this run but the C1 agent saw a `gfx_sp_matrix`
|
||||
crash — likely the same explosion-DL `G_MTX` (C2m) as Jungle, timing-
|
||||
dependent (D117 nondeterminism). Cradle renders only 55% — watch for a
|
||||
partial-load issue when it gets a playtest. Frigate/Statue vary 68–90%
|
||||
run to run.
|
||||
|
||||
## Crash classes (most-impactful first)
|
||||
|
||||
### C1 — chr / AI-record pointer deref — FIXED (D123)
|
||||
**RESOLVED.** Root cause: `tools_pc/d88_propdefs.py` (D122's `OBJ_TAIL_DESC`)
|
||||
zeroed the widened `VehichleRecord/AircraftRecord.ailist` slot instead of
|
||||
carrying its pre-populated int AI-list id, so `prop.c:1764/1786` →
|
||||
`ailistFindById(0)` → `GAILIST_AIM_AT_BOND` → `ai()` ran a CHR aim list
|
||||
against `ChrEntityp==NULL`. Fixed via `OBJ_ID_WORDS`. See §F/§H D123.
|
||||
Dam/Frigate/Statue/Cradle now PASS; Runway/Streets fall through to C2.
|
||||
|
||||
<details><summary>original triage notes</summary>
|
||||
|
||||
`chrIsNotDeadOrShot(ChrRecord *self)` faults on `self->actiontype` with
|
||||
`self` bogus (Dam: Rdi=0x1401296a0 → an image address; fault addr 0x8).
|
||||
Reached from `chraidata.c:61` (`m_AimAtBond[]` AI-list region — backtrace
|
||||
frame is inside the global AI-list rodata blob, i.e. the return address
|
||||
was corrupted OR an AI-list entry is being called as a function pointer).
|
||||
Pointer-width / BE-rodata family (PORT-LEARNINGS §A / §C): a per-level
|
||||
`Usetup*Z` guard record (`GuardRecord` type 9 / `GuardAttributeRecord`
|
||||
type 18) or chr-spawn pointer not converted / not pointer-width-adjusted,
|
||||
OR the AI-list bytecode pointers in `chraidata.c` rodata are BE and get
|
||||
called raw. **Biggest single win — 6 levels.**
|
||||
|
||||
</details>
|
||||
|
||||
### C2 — fast3d bad texture pointer (D124) — SPLIT into two causes
|
||||
**Jungle (`0xabcd0824`) — FIXED (D124).** `gimgSyncCompiledGlobalDLs()`
|
||||
never copied the resolved texture pointers into the compiled
|
||||
`globalDL_0xNNN` explosion DLs (slot-detect keyed on a marker `texLoad()`
|
||||
had already erased); the arrays kept link-time `IMAGESEG` words and the
|
||||
first explosion-DL draw fed `0xABCDxxxx` into fast3d. Latent on every
|
||||
level. Fix: `port/src/gimgfixup.c` detects the slot from the compiled
|
||||
array. Jungle now renders ~300 frames then hits a *separate* explosion-DL
|
||||
`G_MTX` crash (`gfx_sp_matrix` gfx_pc.cpp:1046 — D75/matrix family).
|
||||
|
||||
**Facility (`0x72181ee8`) — NOT fixed, separate cause.** Bogus `G_SETTIMG`
|
||||
w1 in a model/prop GDL produced by the runtime `texLoadFromGdl()`
|
||||
relocation in `sub_GAME_7F0762E0` (`objecthandler_2.c:82`): its model-GDL
|
||||
output `dst` pointers are non-16-aligned (N64 8B vs PC 16B `Gfx` stride
|
||||
mix in the `replacementgdl`/`name` offset math). Open D80/D82/D83
|
||||
"model/room GDL runtime conversion unverified" area — shared infra, needs
|
||||
a dedicated pass. Files: `src/game/objecthandler_2.c`, `src/game/tex.c`
|
||||
(`texLoadFromGdl`), `src/game/model.c` (`modelNodeReplaceGdl`).
|
||||
|
||||
**M-14 update (partial, still NOT fixed):** crash cmd pinned —
|
||||
`G_SETTIMG w0=0xfd900000 w1=0x72181ee8` where `w1` is a garbage
|
||||
`tex->data` from the texture-pool path (`texLoadFromModelFileHeader` →
|
||||
`texLoad`), not solely the GDL command-stream copy. Every model file
|
||||
loads at an odd (`&15==1`) `objheader->Switches` base
|
||||
(`mempAllocBytesInBank` does no alignment) and `delta&15` != 0.
|
||||
Full write-up + next-step probe plan: PCPortResearch §F D124-Facility
|
||||
addendum. Likely fix site: the `d43_emit.py`/`pcmodels` sidecar's model
|
||||
texture-blob offsets (N64 8B-Gfx vs PC 16B-Gfx GDL extent).
|
||||
|
||||
### C3/C6 update (M-14/M-15, D125) — offline pipeline RULED OUT, bug is runtime
|
||||
Symptom: converted `propDefs` blob in RAM does not match the offline output
|
||||
(after record 0 it is zeros/garbage), so the runtime `sizepropdef()` walk
|
||||
drifts and `setupDoor`/`modelLoad` get the wrong `pdefIndex`/modelid.
|
||||
|
||||
**M-15: the offline side is proven correct.** `tools_pc/d125_check.py`
|
||||
byte-compares the emitted `pccg.bin` propDefs slice (post-RZ-roundtrip, at
|
||||
the relocated header offset) to `convert_stream()` → **MATCH for all 21
|
||||
levels**. The M-13 overseer hypothesis (pass-1 delta / `pd_end` mismatch in
|
||||
`d88_emit.py` ~L308–340) is **disproven**: instrumented run shows
|
||||
`tiled_pd_end == H[intro]` and `_n64len == pd_end - pd_start` exactly for
|
||||
all 21, next region always `intro`. `sizepropdef()` PORT strides re-checked
|
||||
vs `PROPDEF_PC_BYTES/4` — all 28 match.
|
||||
|
||||
**→ The corruption is at/after runtime load.** Prime suspect:
|
||||
`decompressdata()` truncation in `port/src/rzdecomp.c` for the larger
|
||||
converted setup files (the `while (ret == Z_OK)` inflate loop), or the
|
||||
STAGE bank buffer / `langLoadToAddr` (`prop.c:1274`) overwriting the tail.
|
||||
Next step: probe `decompressdata()` `ret`/`produced` vs expected size for
|
||||
`UsetupsevbZ`. See §F/§H D125.
|
||||
|
||||
### C3 — Aztec FIXED (D125); Bunker2 residual (C3r)
|
||||
**Aztec** was D125 (boundpad plink-name blob drift from the
|
||||
`d88_emit.py:374` slice-width bug corrupting `stanPackId` → NULL stan →
|
||||
door `model=NULL`). Fixed + regen'd, Aztec PASSES.
|
||||
|
||||
**Bunker2 (C3r)** — `door7F054FB4` `propobj.c:13536`, `var_s1` = `0xffff..`
|
||||
walked from `linkedDoor`. `door->linkedDoor` is resolved at setup from
|
||||
`door->linkedDoorOffset` (`prop.c:1204-1206`, N64 struct word 32 / 0x80) —
|
||||
a read-before-write int id, D123's `ailist` pattern. `d88_propdefs.py`'s
|
||||
DOOR handler (`DOOR_TAIL_PTR_WORDS`, tail loop from word 32) vs the
|
||||
compiled PC `DoorRecord` layout is the suspect: either `linkedDoorOffset`
|
||||
lands at the wrong PC byte, or a tail pointer word's 8-align cursor
|
||||
diverges from the compiler's struct layout. Bunker2 has linked
|
||||
double-doors; Aztec's doors are singletons (`linkedDoorOffset==0`) so it
|
||||
never exercised this path. Needs `offsetof`/`sizeof` cross-check of PC
|
||||
`DoorRecord` vs the converter cursor. Files: `tools_pc/d88_propdefs.py`,
|
||||
`src/bondtypes.h` (DoorRecord), `src/game/prop.c`, `src/game/propobj.c`.
|
||||
|
||||
### C4 — prop tile-walk room deref (1: Depot)
|
||||
`prop.c:902` — `*arg1 = sp4C->room` where `sp4C` came back bad from
|
||||
`walkTilesBetweenPoints_NoCallback`. Likely a BG tile/room table not
|
||||
converted for Depot. Files: `src/game/prop.c`, `src/game/bg.c`, BG
|
||||
sidecar.
|
||||
|
||||
### C5 — bg portal points (1: Control)
|
||||
`bg.c:5723` — `portal_pts = g_BgPortals[idx].offset_portal; portal_pts->numPoints`.
|
||||
`offset_portal` unresolved / BE. Via `chrprop.c:62` (chr line-of-sight
|
||||
through portals). Files: `src/game/bg.c`, BG portal sidecar.
|
||||
|
||||
### C6 — modelLoad PitemZ header (1: Surface2)
|
||||
`loadobjectmodel.c:393` — `PitemZ_entries[modelid].header->RootNode`,
|
||||
`header` NULL / `modelid` OOB. Direct D122 continuation (prop/item model
|
||||
id from a propDef record). Fault addr 0x0. Files: `tools_pc/d88_propdefs.py`,
|
||||
`tools_pc/d43_emit.py`, `src/game/loadobjectmodel.c`.
|
||||
|
||||
### C7 — sndSetupSound (1: Surface1)
|
||||
`snd.c:653`. Audio is a parked subsystem (Phase 3) but this is a hard
|
||||
crash on load, not silence. May be dodge-able with a narrow guard until
|
||||
audio lands. Files: `src/snd.c`, `port/src/` audio stubs.
|
||||
|
||||
## Next (1 crash)
|
||||
1. **C2m** Jungle `-level_37` — renders ~300 frames then an explosion-DL
|
||||
`G_MTX` (`gfx_sp_matrix` gfx_pc.cpp:1046, fault 0x401c68e0), D75/matrix
|
||||
family. NOT related to D130 (Facility/Runway were a font-relayout bug, now
|
||||
fixed). `docs/BRIEF-C2gdl-model-reloc.md` is now largely obsolete — its
|
||||
model-GDL-relocation premise was disproven (D130); the Jungle crash is a
|
||||
compiled/explosion-DL matrix pointer, closer to D75/D124-Jungle territory.
|
||||
|
||||
Everything else loads + renders + survives the no-input window.
|
||||
Hand `docs/LEVEL-PLAYTEST.md` to the user for the WS6 completion pass.
|
||||
|
||||
Re-run `tools_pc/level_sweep.sh` after each class fix; keep
|
||||
`-level_09`/`-level_20` green (`tools_pc/framediff.py`).
|
||||
|
||||
---
|
||||
|
||||
## With-input crash sweep (M-30, 2026-08-31)
|
||||
|
||||
Scripted controller-0 input (walk fwd + fire pulses + turns) via
|
||||
`GE_INPUTSCRIPT`, ~46-70 s per level, `GE_PCDUMP` capture. Checks
|
||||
`ge007.crash.log` + VI-pacemaker health. Machine was loaded (concurrent
|
||||
agents) → frame progression slow on some runs but VI posts steady = no
|
||||
hang. All retried on a settled machine where slow.
|
||||
|
||||
| # | Level | With-input result |
|
||||
|---|----------|-------------------|
|
||||
| 33 | Dam | PASS (frame 300+ dump, no crash) |
|
||||
| 09 | Bunker1 | PASS (VI pacemaker healthy, no crash; slow under load) |
|
||||
| 20 | Silo | PASS (VI pacemaker healthy, no crash; slow under load) |
|
||||
| 23 | Control | PASS (frame 600+, no crash) |
|
||||
| 32 | Egypt | PASS (frame 600, no crash — first run was load-slow, clean on retry) |
|
||||
| 54 | Cuba | PASS (frame 1200+, no crash) |
|
||||
| 39 | Caverns | PASS — **D158 (KF7 muzzle-flash `stackpad2[-8]`) holds**, fire no longer crashes |
|
||||
|
||||
Earlier this session (per M-30 handoff), with-input PASS:
|
||||
Facility 34, Runway 35, Surface1 36, Frigate 26, Statue 22, Bunker2 27,
|
||||
Surface2 43, Streets 29, Depot 30, Train 25, Jungle 37, Cradle 41, Aztec 28.
|
||||
|
||||
**With-input sweep: 21/21 crash-free.** No new ABI/layout crashes found.
|
||||
|
||||
### stackpad / negative-stack-index audit
|
||||
`grep -rnE '\)\[-[0-9]|[a-z0-9_]\[-[0-9]+\]|stackpad' src/game/*.c src/*.c`:
|
||||
only `gunfire.c:830/836` (`((f32*)stackpad2)[-8]`) is a true decomp
|
||||
register-alloc stackpad hack — already fixed by D158 (`#ifdef PORT`
|
||||
real local `kf7_flashscale`). `front.c:5246-47` declares `s32 stackpad;`
|
||||
locals but never negative-indexes them (benign, and front.c is off-limits).
|
||||
All other `[-N]` hits are legitimate pointer walks (`gun.c` spline
|
||||
`current[-1]`, `stan.c` `stack[-1]`, `initpathtablelinks.c`
|
||||
`validationGroupCursors[-3]` already D89-handled, `options.c` `vtx[-1]`,
|
||||
`decompress.c` `src[-1]`). `othermodemicrocode.c:368 (aa)[-4]` is a
|
||||
texture-pool pointer walk, not a stack hack. **No further stackpad bugs.**
|
||||
@@ -0,0 +1,286 @@
|
||||
# Texture Glitch Analysis — PC Port
|
||||
|
||||
**Original diagnosis:** 2026-08-29 (read-only) · **Status table refreshed:** 2026-08-31 (M-31)
|
||||
Companion artifacts: `<local-path>` (outside repo).
|
||||
|
||||
---
|
||||
|
||||
## 0. Current status (M-31) — READ THIS FIRST
|
||||
|
||||
The 2026-08-29 body below is the original diagnosis. Several root causes have
|
||||
since been fixed or retracted. Authoritative current state:
|
||||
|
||||
| RC / bug | 2026-08-29 verdict | Current status (M-31) | Where |
|
||||
|---|---|---|---|
|
||||
| **RC2** — mip-chain contamination (LOD rows uploaded as image rows) | open | **FIXED** — `import_texture()` clips a full-width LOD block to the SETTILESIZE base height; GL builds correct mips. Knob `Video.FixMipTextures` (**default on**; `=0` = old byte-identical-to-golden behaviour). | `port/fast3d/gfx_pc.cpp`; §F RC2 |
|
||||
| **RC4** — palette "off-by-one" in `palette_to_rgba32` | proposed 1-line fix | **RETRACTED** — the current decode is correct **RGBA5551**; the "spec" line in §2 was ARGB1555. Applying the "fix" rotates every channel + moves alpha. **Do not touch.** | `gfx_pc.cpp:~835`; §2 RC4, §F |
|
||||
| **D159** ("interlaced" / venetian-blind textures, esp. front-end photos) | not yet identified | **FIXED** — `texSwapAltRowBytes` (N64 RDP odd-line TMEM XOR compensation, which fast3d does not emulate) is a `#ifdef PORT` no-op. This was the user's "interlaced textures" report. | `src/game/image.c`; §F D159 |
|
||||
| **D161** — Depot (`-level_30`) ceiling blue speckle + radial rays (was §3 "blue ground" / B2) | attributed to RC2+RC3+RC4+fog | **FIXED** — a 16×16 **CI8 tile drawn with `G_TT_NONE`** (TLUT disabled) was palette-looked-up against a stale `rdp.palette`. fast3d now routes CI+`G_TT_NONE` → I (intensity). Not RC2/RC3/RC4/filtering. | `port/fast3d/gfx_pc.cpp import_texture()`; §F D161, PORT-LEARNINGS §D |
|
||||
| **RC1** — wallet-Bond photo (mode-select / folder screen) garble | "implement G_SETTEX" | **PARTIALLY OPEN.** The *interlace comb* is gone (D159). The photo now renders as a recognisable grayscale portrait but is still **180°-rotated / mis-transformed** → **D75 front-end-model family** (model-GDL transform, not a fast3d opcode gap — `texLoadFromGdl` already expands G_SETTEX on PC, see §6b). Parked, cosmetic, below crash work. | §6b; GRAPHICS-BACKLOG D149; §F D75/D80/D82/D83 |
|
||||
| **RC3** — wrap-period mismatch (non-PoT dims "repeat oddly") | open | **IN PROGRESS.** The dead D74 pre-wrap block is now opt-in as `Video.WrapFix` (M-29, **not default** — the naive one-liner activates never-run code and boot-crashes `-level_09`; needs the hoist-out-of-vertex-loop rework + per-level visual check). | `gfx_pc.cpp`; GRAPHICS-BACKLOG B1; §F D74 |
|
||||
| RC1 (original) — G_SETTEX (0xc0) is `G_NOOP` in fast3d | "implement the command" | **NOT the blocker.** `src/game/tex.c texLoadFromGdl` already expands every G_SETTEX into a standard load sequence under `#ifdef PORT`; fast3d normally never sees raw 0xc0. See §6b. | §6b |
|
||||
|
||||
Everything from `## 1.` down is the original 2026-08-29 text, left intact for
|
||||
the reasoning trail. Cross-check any "open / pending / proposed" wording there
|
||||
against this table.
|
||||
|
||||
## 1. Reported symptoms
|
||||
|
||||
1. **In general:** textures repeat oddly with weird artifacting.
|
||||
2. **Depot (`-level_30`):** loads "completely wrong" textures — the ground is drawn a
|
||||
glitched **blue** that is not right (visible in attract mode).
|
||||
3. **Character models:** obvious texture artifacts during the credits roll.
|
||||
|
||||
## 2. Root causes (confirmed by code + ROM evidence)
|
||||
|
||||
> **M-31 status overlay** (see §0): RC2 **FIXED** (`Video.FixMipTextures`), RC4 **RETRACTED**,
|
||||
> RC1 superseded — the "interlaced" symptom was **D159** (`texSwapAltRowBytes`), now **FIXED**;
|
||||
> the Depot "blue ground" was **D161** (CI8 + `G_TT_NONE`), now **FIXED**. RC3 in progress
|
||||
> (`Video.WrapFix`, opt-in). RC1 residual = 180°-rotated front-end photo = D75 family, open.
|
||||
|
||||
| # | Cause | Location | Explains | Fix class |
|
||||
|---|-------|----------|----------|-----------|
|
||||
| RC1 | `G_SETTEX` (0xc0) is a **no-op** in fast3d | `port/fast3d/gfx_pc.cpp:2443` (`case G_NOOP`) | #3 character/model artifacts | ~~implement the command~~ — **not the blocker**: `texLoadFromGdl` already expands it on PC (§6b); residual = D75 model transform |
|
||||
| RC2 | **Mip-chain contamination**: base + all LODs uploaded as one GL image | `src/game/tex.c:330` (`texGetDepthAndSize` sums all LODs) → `gfx_dp_load_block`/`import_texture_*` | #1, #2 noise bands, wrong scale, minification garbage | **FIXED** — clip LOD block to base-tile height; `Video.FixMipTextures` default on |
|
||||
| RC3 | **Wrong wrap period**: N64 wraps at `2^ceil(log2(dim))`, GL wraps at uploaded image size; D74 pre-wrap block is dead (`G_TX_WRAP == 0`, `include/PR/gbi.h:391`) | `port/fast3d/gfx_pc.cpp` UV math in `gfx_sp_tri1` + D74 guard | #1 "repeats oddly", squash/stretch on non-PoT dims | **DONE (D167), behind `Video.WrapFix` default OFF** — mask-based wrap period in the hoisted pre-wrap block; needs a human eyeball on Depot before default-on. See §6d |
|
||||
| RC4 | ~~**Palette off-by-one** in `palette_to_rgba32`~~ — **RETRACTED (M-30)**: current decode is correct RGBA5551 | `port/fast3d/gfx_pc.cpp:835` | nothing — the "spec" line was ARGB1555 | **do not change** |
|
||||
|
||||
### RC1 — G_SETTEX no-op (character artifacts)
|
||||
|
||||
- GE's `gbi_extension.h` defines opcode 0xc0 as `G_SETTEX` (`gsSPUseTexture`):
|
||||
`w0[3:0]=type, [9:6]=shiftt, [13:10]=shifts, [17:16]=tile, [19:18]=cmt, [21:20]=cms`;
|
||||
`w1 = minlevel<<24 | detail_id<<12 | texture_id`.
|
||||
- **Full ROM scan: 508 of 512 model files contain G_SETTEX — 43,314 commands total**
|
||||
(top emitters: `PwalletbondZ` 2830, `CbaronsamediZ` 976, `GautoshotZ` 975).
|
||||
- Worked example `CheadbrosnanZ`: 47 G_SETTEX commands; texture_ids (1624–1626 = 32×64,
|
||||
1607 = 64×32, 1608 = 1×1 null) exactly match the model's `Textures[]` table; the GDL
|
||||
interleaves them per mesh part. All use tile=2 (`G_TX_RENDERTILE`).
|
||||
- On N64, gmain.s dispatches 0xc0 via the jump table at offset 0xbc (top-2-bits bucket 3);
|
||||
it rebinds the render tile to the texture identified by `texture_id`.
|
||||
- Our fast3d hits `case G_NOOP: break;` — **every mesh part renders with stale tile state**
|
||||
→ wrong/shifted textures on characters. This is why the credits roll (many models, many
|
||||
texture switches) shows obvious artifacts.
|
||||
- The in-code comment at `gfx_pc.cpp:2443` ("the game never emits it") is **wrong for GE** —
|
||||
presumably inherited from PD assumptions. The PD port also no-ops 0xc0, so there is no
|
||||
reference implementation to copy; implement from gmain.s + gbi_extension.h.
|
||||
|
||||
### RC2 — Mip-chain contamination (floor/room noise)
|
||||
|
||||
- `texGetDepthAndSize()` (`src/game/tex.c:330`) sums **all** LOD levels, so one
|
||||
`gDPLoadBlock` carries base + full mip chain. fast3d uploads that as a single GL image:
|
||||
a 65×65 floor texture becomes ~65×134, rows below the base being raw mip bytes.
|
||||
- GL then wraps at the *uploaded* size and `glGenerateMipmap` + the never-disabled
|
||||
`GL_LINEAR_MIPMAP_LINEAR` min-filter (`gfx_opengl.cpp:59`) sample **mips of a
|
||||
garbage-stacked image**. Distant surfaces (attract-mode camera!) minify into noise.
|
||||
- ~75% of Depot's textures carry full mip chains (lod 6–7), so this is maximal there.
|
||||
- N64 per-LOD tile selection is already emulated by the D107 rule (always base tile,
|
||||
offset 0) — GL mips are pure poison in this port; disable them.
|
||||
|
||||
### RC3 — Wrap period mismatch ("repeats oddly")
|
||||
|
||||
- N64 wraps at the tile mask period `2^ceil(log2(dim))` (`texDimensionToMask`,
|
||||
`src/game/tex.c:361`). GL `GL_REPEAT` wraps at actual image size.
|
||||
- Depot's large room surfaces are **non-power-of-two**: 65×65, 56×56, 96×48, 128×32
|
||||
(I4/IA4, non-zlib). E.g. 65 wide: N64 period 128 vs GL period 65 → pattern squashed 2×
|
||||
horizontally; with RC2 the vertical period is ~2× too long and half of it is noise.
|
||||
- The D74 pre-wrap UV block never runs: guard tests `cms & G_TX_WRAP` but
|
||||
`G_TX_WRAP == 0` (`include/PR/gbi.h:391`) — dead code (already parked in
|
||||
`docs/GRAPHICS-BACKLOG.md`).
|
||||
|
||||
### RC4 — Palette off-by-one — **RETRACTED (M-30): the current code is correct.**
|
||||
|
||||
The "N64 RGBA16 spec" line below is **ARGB1555**, which is *not* what the N64 uses.
|
||||
N64 `G_IM_FMT_RGBA` / `G_IM_SIZ_16b` is **RGBA5551**: `RRRRR GGGGG BBBBB A`
|
||||
(R = bits 15-11, G = 10-6, B = 5-1, A = bit 0).
|
||||
|
||||
```c
|
||||
// fast3d (current, CORRECT): r = v>>11; g = (v>>6)&0x1f; b = (v>>1)&0x1f; a = v&1
|
||||
// the "spec" line here was wrong: r = (v>>10)&0x1f; g = (v>>5)&0x1f; b = v&0x1f; a = v>>15
|
||||
```
|
||||
|
||||
Both `palette_to_rgba32` (`gfx_pc.cpp:~835`) and `import_texture_rgba16`
|
||||
(`~651`) use the same RGBA5551 decode and agree with each other. The palette is
|
||||
`PD_BE16`-swapped at load (`gfx_dp_load_tlut`), so `palentry` is already
|
||||
correctly ordered. **Applying the "fix" would rotate every channel and move
|
||||
alpha to the wrong bit — do not do it.** If a hue problem is ever confirmed
|
||||
in-game, it is upstream (TLUT byte order at the converter, or a wrong
|
||||
`rdp.palette_fmt`), not this function.
|
||||
|
||||
## 3. Depot-specific analysis ("why blue ground, only there?")
|
||||
|
||||
**Depot is not special — it is the worst case of RC2+RC3+RC4.**
|
||||
|
||||
- **Ruled out with data:**
|
||||
- *Texpool exhaustion / D85 placeholder path:* a real Depot load (env-gated log) shows
|
||||
342 unique texture loads, ~326 KB compressed into the 450 KB stage pool, **zero**
|
||||
`D85TEX pool-full` events. The floor is not sampling a garbage pointer.
|
||||
- *Broken decompression:* `texInflateZlib`/`texInflateNonZlib` are faithful decompilations;
|
||||
the format was independently re-derived offline (§4) and decodes byte-exact.
|
||||
- **What Depot's floor actually is** (decoded from ROM): non-zlib **I4/IA4 grayscale with
|
||||
full 7-level mip chains and non-PoT dimensions** (texnums incl. 85/86, 508, 618, 631,
|
||||
779–782, 836, 909–911, 989–996). Exactly the class RC2+RC3 mangle hardest.
|
||||
- **The blue:** nothing in Depot's texture set is actually blue (decoded color stats are all
|
||||
industrial grays/browns, e.g. avg rgb 107/107/108, 147/117/112). The perceived blue =
|
||||
garbage I4 values sampled under minification (attract camera is far → mip-filtered noise)
|
||||
+ Depot's fog table (`src/game/bgfog.c`, LEVELID_DEPOT entry) tinting distant surfaces
|
||||
cold. RC4 then shifts whatever hue survives.
|
||||
- BUNKER1 "renders recognisably" because its surfaces hit the same bugs less severely
|
||||
(PoT dims / fewer visible garbage rows at typical camera distances).
|
||||
|
||||
## 4. Texture pipeline reference (learned during this investigation)
|
||||
|
||||
Useful ground truth for whoever implements the fixes:
|
||||
|
||||
- **ROM layout:** `g_Textures[N]` = segment offset; texture N occupies
|
||||
`[g_Textures[N], g_Textures[N+1])` in the images segment (`_imagesSegmentRomStart`,
|
||||
ROM file offset 0x8F7DF0 in the NTSC dump). `assets/images.def` cumulative sizes match the
|
||||
logged runtime offsets **exactly (0/342 mismatch)** — verified against the D66 log.
|
||||
- `texLoad` (`src/game/image.c:~2450`) copies from `(thisoffset & ~15)` and re-points
|
||||
`compptr += (thisoffset & 7)`; empirically every blob decodes at the listed offset (+0).
|
||||
- **Blob header byte:** `u(1) z(1) lod(6)`. `z=1` → zlib path, else per-image
|
||||
format(4)/w(8)/h(8)/comp(4) + Huffman/RLE/lookup payloads.
|
||||
- **Zlib path** (`texInflateZlib`, `image.c:169`): MSB-first bitstream —
|
||||
format(8), numcolours(8)+1, palette nc×16; per image: width(8), height(8), then a
|
||||
**rarezip stream = 2-byte header `0x11 0x72` + raw deflate** (GE's `src/game/zlib.c` is
|
||||
stock gzip-1.2.4 inflate). Output rows are stored **tight**; `texAlignIndices`
|
||||
(`image.c:340`) repacks to 8-byte-aligned rows (CI8: 1 B/px; CI4: 1 B/2 px, high nibble
|
||||
first).
|
||||
- **`texSwapAltRowBytes`** (`image.c:2191`) is applied to **every level of every texture on
|
||||
every code path** (calls at 260, 300, 312, 316): odd rows get adjacent u32 groups pairwise
|
||||
swapped. The post-swap RAM buffer is what gets uploaded — any offline tool or dump must
|
||||
account for this.
|
||||
- Palette is appended to the buffer big-endian after all images; `G_LOADTLUT` uploads it.
|
||||
- `u=0` + lod≥2: mips are generated at load by `texShrinkPaletted` (2×2 palette average).
|
||||
|
||||
## 5. Open questions / hypotheses
|
||||
|
||||
- **H-A (high confidence):** Depot blue ground = RC2+RC3+RC4+fog, per §3. Confirm in-game
|
||||
after fixes; the before/after capture is the test.
|
||||
- **H-B (high confidence):** credits-roll model artifacts = RC1. Confirm by implementing
|
||||
G_SETTEX and re-capturing the roll.
|
||||
- **H-C (unresolved, non-blocking):** the offline contact sheet "still looks off" to the
|
||||
reviewer despite stage-by-stage verification of the decoder (§4; exact byte-count matches,
|
||||
verbatim function copies, clean odd/even row-boundary statistics). Possible explanations:
|
||||
32×32 CI8 textures are inherently noisy at native res; small-cell presentation; or a
|
||||
residual misunderstanding of the final layout. Fallback if it ever matters: a standalone C
|
||||
harness running GE's actual `texInflateZlib` on the ROM, byte-diffed against the Python
|
||||
decoder. **Not on the critical path** — the in-game fixes don't depend on it.
|
||||
|
||||
## 6. Fix plan — ORIGINAL (2026-08-29); see §0 for what actually shipped
|
||||
|
||||
> **M-31 outcome:** item 2 (mip) shipped as `Video.FixMipTextures`; item 4 (palette)
|
||||
> retracted; the "interlace" was `texSwapAltRowBytes` not on this list (D159, fixed);
|
||||
> the Depot roof was CI8+`G_TT_NONE` not this list (D161, fixed); item 5 (dump hook)
|
||||
> shipped as `GE_DTEX` + `GE_TEXDUMP`. Items 1 (G_SETTEX) and 3 (wrap) not done as
|
||||
> written — see §0 / §6b.
|
||||
|
||||
1. **G_SETTEX** (`gfx_pc.cpp` dispatch): rebind render tile to the texpool entry for
|
||||
`texture_id` per gmain.s semantics. Biggest single visual win (characters).
|
||||
2. **Mip contamination:** upload base level only in `gfx_dp_load_block`/`import_texture_*`;
|
||||
drop `glGenerateMipmap` and the `MIPMAP_LINEAR` min-filter default. Fixes Depot floor noise.
|
||||
3. **Wrap period:** wrap at the N64 mask period (`texDimensionToMask`) — fix the dead D74
|
||||
guard (`cms == G_TX_WRAP`), extend to the T axis and non-PoT dims.
|
||||
4. **Palette off-by-one** (`gfx_pc.cpp:835`): one-line-class fix per channel.
|
||||
5. **Verification hook:** env-gated dump of the actual uploaded GL image + tile setup for
|
||||
the first N texLoads (~10 lines in fast3d) → one Depot run gives true ground truth and a
|
||||
before/after reference (also settles H-C if it ever resurfaces).
|
||||
|
||||
Each item gets its own Dxx entry in `docs/internals.md` §F/§H at commit time
|
||||
(proposed: next free labels after D126 — check the §F index first; a stray "D160" appears
|
||||
once in the doc and may need reconciling).
|
||||
|
||||
## 6b. M-30 follow-up — implementation notes (before you touch RC2)
|
||||
|
||||
Re-checked against the current tree while chasing the user's "interlaced
|
||||
textures" screenshot (`screenshots/interlaced texture example.jpg` — main menu,
|
||||
wallet-Bond photo + right panel show alternating-row tearing).
|
||||
|
||||
- **RC1 (G_SETTEX) is not a simple `case` add.** `src/game/tex.c texLoadFromGdl`
|
||||
already *expands* every G_SETTEX (opcode 0xc0 == `G_NOOP` slot) in a model GDL
|
||||
into a full standard load sequence (`texLoadFromTextureNum` + `texFindInPool` +
|
||||
`texWriteTextureCmd`) written into a replacement GDL, and
|
||||
`src/game/objecthandler_2.c` (`~line 82`) drives that per display list at model
|
||||
load + `modelNodeReplaceGdl`s the node. It **is** `#ifdef PORT`-ported. So on PC
|
||||
the RSP/fast3d should normally *not* see raw G_SETTEX. The user's crash log
|
||||
showed fast3d walking into `0xfafbfbfc…` garbage (opcode 0x00, **not** 0xc0) —
|
||||
i.e. the model GDL pointer / expansion is broken for the front-end wallet
|
||||
models, not "an opcode is missing." This is the **D75 front-end-model family**
|
||||
(D80/D82/D83 model-GDL relocation, `objecthandler_2.c`), not a fast3d opcode
|
||||
gap. Implementing G_SETTEX in fast3d would be a fallback that only helps models
|
||||
that reach it with an otherwise-intact DL.
|
||||
- **RC2 (mip contamination) — the naive height clamp was already tried and
|
||||
reverted.** `gfx_pc.cpp import_texture()` ~line 917 has a comment: a prior
|
||||
branch that set height = `line * tile.height` was removed because it "dropped
|
||||
mip chains" and "truncated sub-tiled textures" (Rare-logo `D_02005FF0` 20×3
|
||||
tile uploaded as 32×3). So the fix must distinguish "extra rows are mip levels"
|
||||
(drop) from "tile is a window into a larger image" (keep). The signal is
|
||||
`rdp.tex_lod` (passed to every `import_texture_*` as `gen_mipmaps`): when LOD is
|
||||
on and `size_bytes/row > tile_height`, the excess is mip data → clamp height to
|
||||
`((lrt-ult)>>2)+1` from `rdp.texture_tile[tile]` **and** pass `gen_mipmaps=false`
|
||||
(don't let GL build mips from the stacked image). When `rdp.tex_lod` is off,
|
||||
keep today's behaviour. Verify against the Rare logo (sub-tile case) + Depot
|
||||
floor (mip case) + BUNKER1 (regression).
|
||||
- **The alternating-row look specifically** may also be `texSwapAltRowBytes`
|
||||
(`image.c:2191`, §4: applied to *every* level of *every* texture on *every*
|
||||
path — odd rows get adjacent u32 groups pairwise swapped). The game swaps the
|
||||
RAM buffer *before* upload, so fast3d should read the swapped bytes — confirm
|
||||
fast3d isn't reading a pre-swap copy, or double-swapping. This is the
|
||||
RenderDoc / §6-item-5 dump task; do that before editing.
|
||||
|
||||
**Bottom line (M-30):** #3 is not a grind-fix. Highest-leverage next step is
|
||||
§6-item-5 (env-gated dump of the actual uploaded GL image + tile setup for the
|
||||
first ~10 texLoads on the main menu and on Depot) to see whether the base image
|
||||
is wrong (decode/swap) or just over-tall (RC2 height). Then RC2 per the
|
||||
`rdp.tex_lod`-gated plan above. G_SETTEX / D75 front-end models are a separate,
|
||||
larger track.
|
||||
|
||||
## 6c. B2 Depot ceiling — ROOT-CAUSED + FIXED (D161, M-31)
|
||||
|
||||
The blue-speckle + radial-ray roof was **not** RC2/RC3/RC4/filtering. `GE_TEXDUMP`
|
||||
(PPM dump of every uploaded texture + a `fmt/siz/palfmt/palidx/pal[0..3]` log
|
||||
line, both env-gated, in `gfx_pc.cpp`/`gfx_opengl.cpp`) identified the surface as
|
||||
**one 16×16 CI8 texture uploaded with `rdp.palette_fmt == G_TT_NONE`** — every
|
||||
other CI texture that frame had `palfmt == 0x8000` (RGBA16). GE draws the Depot
|
||||
roof with `gsDPSetTextureLUT(G_TT_NONE)`: a CI-siz tile with the TLUT disabled is
|
||||
a legit N64 idiom meaning "use the raw 8-bit texel as intensity" (≈ I8).
|
||||
`import_texture()` ignored `palette_fmt` for `fmt==CI` and did a palette lookup
|
||||
against a **stale `rdp.palette`** → 16×16 of blue/magenta noise; the "rays" were
|
||||
that noise aliasing on the receding ceiling plane (hence filtering never helped).
|
||||
|
||||
**Fix:** `gfx_pc.cpp import_texture()` — `fmt_eff = (fmt==CI && palette_fmt==
|
||||
G_TT_NONE) ? I : fmt`, dispatch on `fmt_eff` (CI4→I4, CI8→I8). Narrow (only
|
||||
touches currently-garbage surfaces). Depot corridor + control-room ceilings now
|
||||
render as clean dark/grey industrial roofs; `-level_09/-30/-34` sweep PASS.
|
||||
Full write-up: §F D161.
|
||||
|
||||
## 6d. RC3 non-PoT wrap period — IMPLEMENTED (D167, M-31), default OFF
|
||||
|
||||
`gfx_dp_set_tile` now stores `masks`/`maskt` on the tile (was dropped). The
|
||||
hoisted per-texunit pre-wrap block in `gfx_sp_tri1` (the reworked D74 block —
|
||||
already lifted out of the vertex loop at M-30, indexed by texunit `t`) gained a
|
||||
branch: when the render tile is WRAP (no CLAMP/MIRROR) and `1<<mask != tex_width`,
|
||||
fold the UV at the N64 period `1<<mask`, then clamp the `[dim, 1<<mask)` overflow
|
||||
band (no real texels — a TMEM smear on console) to the last texel. All behind the
|
||||
existing `Video.WrapFix` knob (`GE_WRAPFIX` env overrides it for testing).
|
||||
|
||||
**Captures** (`-level_09/-30/-34/-20`, WrapFix OFF vs ON, `GE_PCDUMP` 6-frame
|
||||
windows): no crashes, 6/6 frames each; Silo ~pixel-identical (phash 0–11),
|
||||
Facility 180–260 pixel-identical, Depot shows small localized texel changes on
|
||||
ceiling/wall cells (dmean 5–9, no structural break). Large per-run frame deltas
|
||||
are all D117 intro-camera-pan nondeterminism.
|
||||
|
||||
**Default kept OFF** — no regression, but a headless structural diff can't confirm
|
||||
the Depot ceiling looks *better*. To finish RC3: run Depot with `Video.WrapFix=1`
|
||||
and eyeball the corrugated roof / repeating wall panels vs default; flip the
|
||||
default in `port/src/video.c` (`cfgWrapFix = 1`) if clean. The overflow-band clamp
|
||||
is an approximation of console TMEM-smear behaviour, not an exact emulation.
|
||||
|
||||
## 7. Artifacts (in `<local-path>`, outside repo)
|
||||
|
||||
| File | What it is |
|
||||
|------|-----------|
|
||||
| `depot_d85.log` | Full Depot run with `GE_D63=1 GE_D85TEX=1` — 342 texLoad records (texnum/thisoff/nextoff/size), zero pool-full events |
|
||||
| `decode_depot_tex.py` | Offline rarezip/bitstream decoder (verified against §4) |
|
||||
| `depot_texsheet.png` | Contact sheet, 342 tiles, labeled by texnum (zlib-decoded only; non-zlib I4/IA4 cells blank) |
|
||||
| `depot_texstats.txt` | Per-texture format/size/lod/color stats for all 342 |
|
||||
| `tiles_big.png`, `tile_*.png` | Individual full-size decodes (760, 313, 1283, 233, 1082, 21, 451, 7) |
|
||||
@@ -0,0 +1,246 @@
|
||||
# Agentic development: goal, method, timeline, assessment
|
||||
|
||||
## Why this project exists
|
||||
|
||||
The playable port is real, but it is not the primary deliverable. The goal was
|
||||
to **test how well coding agents hold up on a large, unfamiliar, low-level
|
||||
codebase** — one with none of the properties that make web-app work easy for
|
||||
an LLM:
|
||||
|
||||
- ~230 translation units of decompiled Nintendo 64 game C, compiled
|
||||
**unmodified**;
|
||||
- big-endian, 32-bit, MIPS ABI assumptions throughout, run on a little-endian
|
||||
64-bit host;
|
||||
- bugs that surface as a fault or a garbled frame hundreds of milliseconds
|
||||
after the actual cause, often in a different subsystem;
|
||||
- a graphics coprocessor (the RSP) that has to be emulated in software before
|
||||
anything draws at all.
|
||||
|
||||
Concretely it set out to validate a **two-agent arrangement**: a
|
||||
locally-hosted open-weight model doing the bulk of the work on a single
|
||||
consumer GPU, a hosted frontier model brought in for a collaborative phase,
|
||||
and — the part that turned out most interesting — the two **handing work back
|
||||
and forth** through shared written artifacts, directed by one human.
|
||||
|
||||
## The setup
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Local agent | `unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL` on a single **NVIDIA RTX 5090**, driven mainly through the **[pi](https://pi.dev/)** coding agent. Unsloth Desktop (a newer local agentic-coding harness) was also trialed but not used significantly. |
|
||||
| Hosted agent | **Claude**, via **Claude Code**, on a Claude Pro subscription — mostly **Sonnet 5**, with **Opus 5** used as an escalation tier for the hardest problems and whenever there was subscription budget to spend on it |
|
||||
| Human | one person: direction, work partitioning, integration, and every build / playtest / frame-capture the agents could not run |
|
||||
| Base | fork of the [GoldenEye 007 decompilation](https://github.com/n64decomp/007) (years of prior work by Larry Ficken / "KholdFuzion" and contributors) |
|
||||
| Reused | the [Perfect Dark PC port](https://github.com/fgsfdsfgs/perfect_dark)'s `fast3d` software RSP — same Rare engine family |
|
||||
|
||||
The port is the GoldenEye-specific porting work **on top of** those two
|
||||
existing bodies of work; it is not a from-scratch reimplementation of either.
|
||||
|
||||
In practice the models formed a **three-tier escalation**: the local model
|
||||
handled well-scoped work, Sonnet 5 took what it stalled on, and Opus 5 was
|
||||
reserved for the bugs that needed the most reasoning held at once — the same
|
||||
"escalate when stuck" move applied at every level.
|
||||
|
||||
## Timeline
|
||||
|
||||
All dates from this repository's own commit history (August 2026).
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
axisFormat %b %d
|
||||
title GoldenEye 007 PC port — from fork to playable front end
|
||||
section Local model - Qwen 3.8 via pi
|
||||
Fork + PC-port scaffolding :m1, 2026-08-16, 1d
|
||||
Full compile + link (~230 TUs) :m2, 2026-08-20, 1d
|
||||
Boot to window (ROM map + SDL2) :m3, 2026-08-20, 1d
|
||||
OS shims, threads, fast3d integration :2026-08-21, 2d
|
||||
First rendered frames :m4, 2026-08-22, 1d
|
||||
Offline asset-conversion pipeline :2026-08-22, 2d
|
||||
Entire intro renders (logos->cast) :m5, 2026-08-24, 1d
|
||||
section Both agents - handoff workflow
|
||||
Claude joins :milestone, 2026-08-27, 0d
|
||||
Stage load; Bunker 1 renders + firefight :2026-08-27, 2d
|
||||
21 solo levels: load + render + no-crash :m6, 2026-08-29, 1d
|
||||
SDL input layer (kbd/mouse/gamepad) :2026-08-29, 1d
|
||||
Front-end flow menu->briefing->start :m7, 2026-08-30, 1d
|
||||
File-backed EEPROM saves :2026-08-31, 1d
|
||||
```
|
||||
|
||||
- **Day 0** (16 Aug): repository forked, PC-port scaffolding added.
|
||||
- **Day 4** (20 Aug): the entire ~230-TU game + libultra set compiles and
|
||||
links as a host binary.
|
||||
- **Day 6** (22 Aug): first real frames render.
|
||||
- **Day 8** (24 Aug): the whole intro sequence renders — logos, gun-barrel,
|
||||
cast roll.
|
||||
- **Day 11** (27 Aug): second agent joins.
|
||||
- **Day 13** (29 Aug): **all 21 solo missions load, render, and survive an
|
||||
unattended play window without crashing.**
|
||||
- **Day 14** (30–31 Aug): front end playable end to end (menu -> mission
|
||||
select -> difficulty -> briefing -> start); file-backed saves.
|
||||
|
||||
So roughly **two weeks**, one person part-time, to take a decompilation from
|
||||
"builds an N64 ROM" to "boots on desktop, renders every solo level, playable
|
||||
through the front end into the early game" — with audio and a set of cosmetic
|
||||
issues still outstanding.
|
||||
|
||||
We are not aware of a prior Nintendo 64 native PC port developed primarily
|
||||
through coding agents, or of one reaching this state on this timeline. That is
|
||||
a statement about how little precedent there is, not a claim of a record.
|
||||
|
||||
## By the numbers
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Calendar time | 16 days (16 Aug – 1 Sep 2026), one person part-time |
|
||||
| Commits on the port | 217 |
|
||||
| Root-caused bugs logged | 160 (`D1`–`D167` in [`findings.md`](findings.md)) |
|
||||
| Handoff sessions | ~30 (`M-2` … `M-32`) |
|
||||
| Game-source files given `#ifdef PORT` ABI edits | 63 files, 241 blocks |
|
||||
| New port-layer / tooling files | 128 |
|
||||
| Port layer | ~17,000 lines C/C++ (`port/`) |
|
||||
| PC asset-conversion tooling | ~6,300 lines Python (`tools_pc/`) |
|
||||
| Outcome | intro + all 21 solo missions render, front end playable; audio not yet done |
|
||||
|
||||
### Commit velocity
|
||||
|
||||
```mermaid
|
||||
xychart-beta
|
||||
title "Commits per day"
|
||||
x-axis ["8/16", "8/17", "8/20", "8/21", "8/22", "8/23", "8/24", "8/27", "8/28", "8/29", "8/30", "8/31", "9/1"]
|
||||
y-axis "commits" 0 --> 60
|
||||
bar [1, 2, 2, 6, 9, 2, 4, 7, 57, 39, 39, 46, 3]
|
||||
```
|
||||
|
||||
The 8/25–8/26 gap is between phases; the jump on 8/28 is the second agent
|
||||
joining, plus the parallel multi-agent "bursts" used near the end.
|
||||
|
||||
### Who did what
|
||||
|
||||
```mermaid
|
||||
pie showData title "Commits by phase"
|
||||
"Phase A - local model, solo (16-24 Aug)" : 26
|
||||
"Phase B - collaborative (27 Aug-1 Sep)" : 191
|
||||
```
|
||||
|
||||
| Milestone / workstream | Primary agent | Weight |
|
||||
|---|---|---|
|
||||
| PC build system, region macros, full compile + link of ~230 units | local model | large |
|
||||
| Boot chain: ROM map, OS-shim layer (`libultra.c`), host threads, dual-mapped DRAM | local model | large |
|
||||
| Software-RSP integration + replacement scheduler (`gesched.c`) | local model | medium |
|
||||
| Offline asset-conversion architecture + first converters (`tools_pc/`) | local model | large |
|
||||
| Intro rendering (logos → gun-barrel → cast) | local model | medium |
|
||||
| Stage load unblocked (`D69`–`D87`) | Claude | medium |
|
||||
| 21-level crash sweep — ~12 crash classes root-caused (`D88`–`D167`) | Claude | large |
|
||||
| SDL input layer (keyboard / mouse / gamepad, mouse-look) | Claude | medium |
|
||||
| Front-end flow (menu → briefing → start), EEPROM saves | Claude | medium |
|
||||
| Parallel struct-layout / converter static audits | local model | medium |
|
||||
| Continuing tasks after Claude hit a usage limit | local model | small |
|
||||
| The hardest structural bugs (matrix handedness, format specs) | Claude | — |
|
||||
| Every build, playtest, frame capture, integration, and direction | human | — |
|
||||
|
||||
**Estimated effort split.** By raw commit count it is ~28% local model /
|
||||
~72% Claude (Phase A entirely local, ~15–20% of Phase B local). Weighting by
|
||||
milestone difficulty rather than commit count — the foundation the local model
|
||||
built is a heavier third of the project than its commit count suggests — the
|
||||
developer's estimate is roughly:
|
||||
|
||||
```mermaid
|
||||
pie showData title "Estimated agent effort (milestone-weighted)"
|
||||
"Claude" : 60
|
||||
"Local model (Qwen 3.8 / pi)" : 40
|
||||
```
|
||||
|
||||
The local model's ~40% is front-loaded and foundational (the build, the boot
|
||||
chain, the RSP wiring, the converter architecture) plus continuous parallel
|
||||
support; Claude's ~60% is the higher-volume Phase-B debugging, the two big
|
||||
discrete features, and the hardest structural bugs. A 27B open-weight model on
|
||||
one consumer GPU carrying the entire foundation of a project like this is the
|
||||
result worth taking away.
|
||||
|
||||
## The handoff workflow
|
||||
|
||||
This is the part worth paying attention to.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
H[Human: direction, integration,<br/>build + playtest verification]
|
||||
C[Claude / Claude Code<br/>frontier, hosted]
|
||||
Q["Qwen 3.8 via pi<br/>open-weight, local RTX 5090"]
|
||||
D[(Shared artifacts:<br/>HANDOFF.md · findings.md · porting-notes.md)]
|
||||
|
||||
H -->|scopes task, budget, files| C
|
||||
H -->|scopes task, budget, files| Q
|
||||
C <-->|reads / appends| D
|
||||
Q <-->|reads / appends| D
|
||||
C -.->|usage limit reached ->| Q
|
||||
Q -.->|hard structural bug ->| C
|
||||
C -->|patch + writeup| H
|
||||
Q -->|patch + writeup| H
|
||||
```
|
||||
|
||||
Both agents worked against the **same three written artifacts**, which is what
|
||||
let them substitute for each other:
|
||||
|
||||
1. **`HANDOFF.md`** — the current state, the immediate next task, and the
|
||||
environment gotchas. Originally a session-to-session note for one agent, it
|
||||
became the **interface between the two agents**: when Claude reached a
|
||||
usage limit mid-problem, the local model picked the task up from the
|
||||
HANDOFF state and continued; when the local model hit a bug that needed
|
||||
deeper structural reasoning, it wrote up where it was and Claude took over.
|
||||
2. **`findings.md`** — the chronological finding log. 160 numbered entries,
|
||||
each a root cause with `file:line` evidence and the fix. New agents (either
|
||||
model) are pointed at the relevant entries before they start.
|
||||
3. **`porting-notes.md`** — the append-only "recurring bug classes" file. The
|
||||
single highest-leverage artifact: it stopped both models from
|
||||
re-deriving the same class of N64->PC bug over and over.
|
||||
|
||||
Around these, the working rules (full detail in
|
||||
[`../dev-process.md`](../dev-process.md)):
|
||||
|
||||
- **file-partitioned tasks** — each agent's task scoped to a disjoint set of
|
||||
files so patches never collided;
|
||||
- **visible budgets** — every investigation task carried an explicit
|
||||
"N build->run cycles" limit and a defined fallback (revert probes, write up
|
||||
with a confidence rating);
|
||||
- **the human owns verification** — building, running the game, capturing a
|
||||
frame, and judging it against N64 reference footage was never delegated.
|
||||
|
||||
## Assessment
|
||||
|
||||
Honest notes, for anyone weighing whether this transfers.
|
||||
|
||||
**Worked well**
|
||||
|
||||
- The **local model carried the groundwork phase** — build system, boot
|
||||
chain, OS shims, and the offline-converter architecture. None of it was
|
||||
rewritten later. A 27B open model on one consumer GPU was genuinely productive
|
||||
on this.
|
||||
- **The written-artifact discipline made agent output compound.** An agent
|
||||
starting cold late in the project was more effective than one early on,
|
||||
because the accumulated notes were good. This is also what made the two
|
||||
models interchangeable on a given task.
|
||||
- **Handoff on limit** turned Claude's usage cap from a hard stop into a
|
||||
slowdown — the local model kept the problem moving.
|
||||
- Bounded behavioural bugs (a truncated pointer, a byte-swap off by one) are a
|
||||
good fit for an agent given a tight loop and a reference to diff against.
|
||||
|
||||
**Worked poorly / needed the human**
|
||||
|
||||
- **Anything requiring the running game.** Build, playtest, capture a frame,
|
||||
decide whether it looks right — that loop was the human's job throughout,
|
||||
and it was the bottleneck.
|
||||
- **Non-deterministic bugs.** Frame-timing stalls and concurrent-build
|
||||
flakiness repeatedly fooled agents into "fixing" regressions that were not
|
||||
real. A number of findings are corrections of earlier findings.
|
||||
- **Long structural bugs** (matrix handedness, a whole serialized-format
|
||||
spec) often ended in a "here is what I know, confidence medium" writeup
|
||||
rather than a fix, even with the budget raised.
|
||||
- **The capability gap is a gradient, not a wall.** The local model was strong
|
||||
on well-scoped work and weaker when a bug needed several interacting facts
|
||||
held at once — those went to Sonnet 5, and the few that stalled Sonnet went
|
||||
to Opus 5. Each tier earned its place on the problems the tier below it
|
||||
couldn't close.
|
||||
|
||||
**Still outstanding** (parked below crash/level work): audio is not
|
||||
implemented; several front-end 3D transforms and some text rendering are
|
||||
wrong. See [`GRAPHICS-BACKLOG.md`](GRAPHICS-BACKLOG.md).
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user