Linux reference-frame set for all 21 levels and the cross-platform golden rule (D521); golden tooling made non-destructive (D524); the gate pins a canonical in-tree save (D529) and the PRNG seed (D532); the capture tool's per-level re-pin bug fixed and the Linux set re-captured under the canonical save (D530, D531); no-arg verify.sh exits non-zero. README and docs visual/consolidation passes; the 15 MB preview gif replaced by stills. Refs: D521 D523 D524 D525 D526 D527 D528 D529 D530 D531 D532
459 KiB
HANDOFF ARCHIVE — session-by-session narrative
Local machine paths are genericised in this archive:
<repo>= this repo's working directory,<repos>= the local source-repos root,<games>/<videos>= the local games / video roots,<python>= the local CPython install,<temp>= a writable native temp dir. Session facts are unchanged.
Reference only. Current state lives in the README "Status" section and
docs/dev/LEVEL-STATUS.md; per-finding detail is infindings.md§F/§H — look up viafindings-index.csv, then read the specific## Dxxentry. This file is the frozen accumulation of prior sessions' handoff briefs (M-2/M-3, plus M-140 → ~M-12 appended 2026-09-15; the M-4–M-11 briefs were never archived — pre-existing gap), 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)
CLAUDE.md(auto-loaded) +AGENTS.mdnon-negotiables.- This file, top-to-bottom.
docs/porting-notes.md— recurring bug classes.- Dispatching a subagent?
docs/dev-process.mdFIRST. 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 Origins, 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/dev/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):
- Input layer (Phase 3) — IN PROGRESS (M-8 agent, §F D118).
port/src/ input.cwas a stub; only a minimal keyboard path inlibultra.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. - 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.cweapon setup (~600-661) +field_87Fgating (~520/587-598) FIRST with aGE_D119printf — does setup even run? Then checkweaponModel.render_pos=dynAllocate'drwmtxlifetime vs thegunRenderFirstPersonGunModels(~1605) consumer (on N64 it aliased the persistenthand->mtxlist; D102 gave it own storage → arena may recycle). AlsoweaponRwPool[192]capacity vsmodelCalculateRwDataLen(mdlhdr)on PC, andflashvisptrword-index assumption (~640). Narrow#ifdef PORTlifetime/sizing fix only — if it needs a behavioural arena change, STOP + write up. Direct-level_09boot may spawn Bond holstered — may need forced weapon state or the attract path to get the gun on screen. Gate to playability. - The HUD/text X-mirror (D116) — DEPRIORITISED, parked in
docs/dev/GRAPHICS-BACKLOG.md. M-11 confirmed the ammo digits use the SAMEtextrelated.cpath (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 indocs/dev/GRAPHICS-BACKLOG.md; they rank BELOW level-progression and crash work. - Rest of the
struct playeroffset pass — D115 fixed the HIGHgunfire.cTHROW* bugs; #6 (watch-preview Model pool) + the broader audit remain (docs/dev/AUDIT-M6-player-offsets.md). - 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"). - 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_09run segfaulted ~frame 1200 the instant a BUNKER guard opened fire:bondwalkItemGetAutomaticFiringRate(gun.c:1334) ←chrlvInitActAttack. ~28 sites punweapons_held[]->chr(really aWeaponObjRecord*) asChrRecord*and read.act_<x>.attack_item, which on N64 aliasesWeaponObjRecord.weaponnum(act union @0x2C + 84 == 0x80). Pointer widening moves the act union to ~0x38 on PC → garbage negative item id → OOBg_ItemStats→ crash. Fix:PUN_ATTACK_ITEM()macro readsweaponnumdirectly;#elsebranch 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 →chrCreateBloodStainPointUsage[]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-0x18ModelRoData_DisplayList_CollisionRecordconversion is incomplete — 6 pointer fields widen the struct andPointUsage/CollisionVerticessub-array endianness+stride aren't handled (onlyCollisionRelatedNodegot the D43/D45u32-vma treatment). Interim:#ifdef PORTcaps both walk loops atnumVertices+8iters + bounds-checksindex→ 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, extendd43_emit.py. Same shape as D69/D88 converter work. - Result:
-level_09now 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). NeedsMOUSE_TURN_GAINbump and/or a matched pitch path; ideally a single tunableMouseAimSpeed. - 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 theaimDY >=/<=C-up/C-down assignment (or defaultMouseInvertY). One-line fix once confirmed against GE pitch sign. - Still TODO from D118: rebinding, gamepad hotplug,
ge007.ininot 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.z64first (§F "data/deletion + recovery"). - Repro:
./build-pc/ge007.x86_64.exe -level_09 -ml0 -me0 -mgfx100 -mvtx50 -mt700 -ma150—-m*are BUNKER1'sboss.cper-level sizes; a bare-level_09skips 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
-level_09now works.osPiReadIowas 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 fromargv[1..].- Sidecar tables patched in
obInit()instead of lazily at first model load — a direct stage boot loaded raw big-endian ROM before. - D88.5 — stan tile-name byte-swap in the converter (0/276 → 273/273 name matches during pad setup).
- D88.6 — intro CAMERA
lang1cis au16pair, not ans32(fixed thelangGetNULL-bank crash). - D89 —
init_path_table_links[-3]OOB → SIGILL fix; NULL-tile guard insub_GAME_7F0B0914. - 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. - D91 —
(s32)&D_800442FC[portalnum]truncation in bg portal cull. - D92 —
chrAllocates32param truncated the ailist pointer;Model.unka0s32field truncated a stored function pointer.
Next steps for the resuming session (render milestone):
- D85 — root cause confirmed (8-byte N64
Gfxvs 16-byte PCGfxin the unconverted per-room DL blob). Runtime widen+bswap fixup being implemented; verify withGE_D69BB=1(real GBI opcodes, not 00/01/02/52), thenGE_PCDUMP+tools_pc/pixcount.pyfor non-degenerate BUNKER1 geometry. See §F "D85 root cause CONFIRMED". Fallback if the widen stalls: soften the foursysFatalError("Bad size…")guards ingfx_pc.cppto skip-with-warning. - 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 intostruct player, cf. D56) is broken independently of the D73 matrix sin/cos fix. Full triage plan infindings.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:
- 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_datapointer growth). Field 0 reads fine; everything after it is reading the wrong bytes entirely. - 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 wholeUsetup*Zformat (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 astools_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
- D88. Get a byte-level spec of
Usetup*Z(start from BUNKER1's file;strResourceis built as"U" + "sev" + "Z"-style name inprop.c:1253-1265— checksetup_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 livestagesetupstruct) into a freshly-populatedg_CurrentSetupis probably simpler than a full sidecar here, but verify against a raw ROM hex dump either way). - 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). - Once BUNKER1 reaches a rendered frame with no fault, revisit D85
(room primary/secondary GDL binaries decode to garbage via
texLoadFromGdl) — useGE_PCDUMP="<range>:10"+tools_pc/pixcount.pyto confirm non-black, non-degenerate content per the original D69 acceptance bar. (This session'sGE_PCDUMPcaptures 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.) - 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 inmodelInitRwData(model.c) + a load-identity probe inload_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 initerate_ramrom_entries_handle_camera_outand consumer trace inramrom_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.pyfor frame captures once D88 is fixed and a frame actually renders real BUNKER1 geometry.- Crash log:
ge007.crash.log(repo root); symbolicate withaddr2line -e build-pc/ge007.x86_64.exe -f -C <pc>(image base0x140000000). - 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 ofps -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 — seedocs/internals.md§F environment reminders for the exact recipe used to root-cause D87.
Non-negotiables (unchanged, see AGENTS.md)
- N64 build files untouched.
- Game logic unmodified except narrow, documented
#ifdef PORTABI/layout exceptions (D86/D87 this session, both logged infindings.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. - 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.
Appended 2026-09-15 — moved verbatim from docs/HANDOFF.md (M-140 → ~M-12)
Ordering note: the material above this divider ends at session M-3. The M-4–M-11 briefs were never archived (pre-existing gap, not lost in this move). The block below runs newest-first (M-140 → ~M-12) and mixes section types (
### ARCHIVE — M-x,## ARCHIVE — D236…pass logs, dated## <date> sessionheadings,## Done this session (M-x)). Normalize to monotonic order only if this file accumulates again.
ARCHIVE — M-140 current-task summary (superseded by M-141 above)
(This whole block, down to the next ARCHIVE heading, is M-140's original CURRENT TASK write-up — release branch cut, D255 root-caused for real, D285/D286 first surfaced. Kept for history; M-141 above is authoritative.)
ARCHIVE — M-139 current-task summary (superseded by M-140)
STANDING DIRECTIVE (M-139b): do NOT push anything to GitHub right now; main is forbidden (no direct pushes, ever — PRs only). All Phase R work lands on local branches for user review. The security/accuracy drafts + release-review design live in gitignored docs/dev/notes/ — never push them; keep untracked docs/security-and-fidelity-status.md out of commits until the Phase F docs batch.
Read docs/dev/notes/ROADMAP-1.0.md's M-139 addendum (top of file) first — it is now the plan of record, with all four planning decisions RESOLVED (M-139b): working-tree extraction APPROVED (branch + sweep + land D266/D271 fixes), PRNG fix APPROVED (port/src/random.c → true MIPS64 shift semantics, verify vs random.s, log a Dxx), cutscene parallel dispatch APPROVED but needs a Claude/local-LLM session (start from the unmerged investigate/d243-cutscene-race branch — its GE_D243 trace is not in main), PAL/JP DEFERRED to late pre-1.0 or 1.1. Also in M-139b: the owned PR-hygiene list for when pushing resumes (rebase+merge #56 D231 QoL — its code is NOT in main; diff-review #86; delete stale squash-merged branches like fix/d230-raw16-bass-swap/feat/dropin-rom-part-a). The user set the path to 1.0 explicitly: Phase R fix small/annoying N64-parity issues, Phase Q QoL wave, Phase F final-release gate; post-1.0 = macOS/ARM + LAN multiplayer (already public in the README Roadmap, which was checked and matches — no README change owed yet). Phase R order: (1) D255 crash (deterministic repro: Facility door-opening computer terminals, Linux-wide — top pickup, it's a crash on a level-progression step in the public "Latest" release); (2) working-tree decision — extract the uncommitted D266 IA4-decode + D271 portal-culling fixes from the D236 probe state onto a branch, run the owed game-wide regression sweep, land or shelve (D271 may overlap D249 distant-geometry drop-out — check first); (3) cheap high-visibility parity items — D252 rainbow particles (root-cause candidate known: import_texture_rgba16 fire-tile width/height), D283 Deck preset skip, D251 F10 bottom-row duplicate, D75 logos; (4) mid-size parity — D245 water seam, D246 edge strips, D249 (after the D271 check), D240/D241 gunshot SFX, D230 music bass instrument; (5) PRNG decision — M-138's port/src/random.c not-bit-exact finding: fix-first (recommended) vs disclose. Cutscenes (D243/D160/D173/D148, bar #4) run in parallel with Phase R as dispatched subagent work, not after it. Four decisions are owed from the user at the bottom of M-139 (working-tree sweep, PRNG fix-vs-disclose, cutscene parallel dispatch, PAL/JP in 1.0 vs 1.1).
ARCHIVE — original RESUME BRIEF from M-139 (superseded, see the M-141 one near the top)
Steps 1–4 of the original M-139 brief are DONE (see M-140 above: release branch cut, R2 extraction landed, PRNG fixed, D255 root-caused and fixed). Pick up at step 5:
- Cheap parity batch (one commit per the cadence preference): D252
rainbow particles (confirm the
import_texture_rgba16fire-tile width/height suspect with one tile dump), D283 Deck preset skip, D251 F10 bottom-row duplicate, D75 logos. - Rebase PR #56 (D231 mute-on-focus-loss + screenshot hotkey — NOT in main; stale base) onto the release branch and merge — in scope for v0.3.0 (QoL is on board by decision). Also scope D282 (gamepad front-end right-stick nav) into this release per M-139c.
- Mid-size parity as capacity allows: D245 water seam, D246 edge strips, D249 (only after step 2's D271 check — already confirmed no overlap, see M-140's D271 finding), D240/D241 gunshot SFX, D230 music bass instrument.
- Dispatchable (local-LLM/Claude sessions): cutscene thread — start
from the unmerged
investigate/d243-cutscene-racebranch (GE_D243trace, not in main); D243 first, D160/D173 data accrues from it. - Optional pickup, not blocking: D285 (new, M-140) — audio-thread SIGSEGV, case identified via a live Deck crash + code read, not yet gdb-confirmed or fixed. Unrelated to D255; fine to leave open into the release if time runs short.
- Sign-off gate: full 21-level sweep + campaign playtest (Windows;
Linux/Deck spot-check — the WSL
Ubuntu-24.04environment set up this session + the user's SSH access make this much cheaper now) + audio pass → tag on the release branch → release→main PR (the one big merge) → PR hygiene (#86 diff-review, delete stale squash-merged branches).
D236 tree investigation: STILL PARKED on park/d236-probes-m139, off release/v0.3.0, not merged. Ninth pass (D280) found no quick win — remaining work is CPU-side room-streaming/instantiation, not a port fix. Do not resume without a specific reason; the D266/D271 fixes worth having were already extracted and landed in M-140.
Commit-cadence note (user feedback, M-139): batch related fixes into one commit/push instead of pushing per individual fix — see docs/dev/notes/COMMIT-CADENCE-PREFERENCE.md. Also: deleting/recreating a release tag repeatedly resets its draft/prerelease flags on GitHub each time — avoid tag churn; get everything ready locally before the first tag push.
ARCHIVE — D236 Surface 1 tree "wall", ninth pass (D280, M-135): fog math VERIFIED N64-faithful, noise class visually confirmed as the painter, depth-race read inverted — no quick win for trees; remaining work is CPU-side instantiation/room-streaming, not a port fix (2026-09-15)
USER ORDER IN FORCE (superseded by v0.2.1 shipping, kept for history): offline only — do NOT push anything. All port changes stay uncommitted for user review.
TL;DR for a fresh session — read D280 in findings.md §F first
User asked: any quick win to easily implement the trees now? Answer: no, and this pass established why:
- Fog is not a bug (D276 owed step #1, closed). Expected per-vertex fog from the game's own constants (RSP ramp 0..154 → fmul=32000/foff=-31744; the noise class's z/w∈[0.992,1.0] → ≈26–28) vs the port's actual values for all 2505 vertices of
0xc81049d8: ±2 (integer truncation only). D276's "low fog" is correct N64 behaviour — on hardware this quad also reaches the screen largely unfogged. - The noise class is definitively the weave's painter (D276 owed step #3, closed).
GE_D278C=1collapse +GE_PCDUMP=1500-1500:1: band HF energy 14.8→2.9, mean 108→121; the band becomes a smooth bright haze. No trees appeared without it — the first unconfounded test (every prior A/B, D268/D273 included, was behind the opaque noise quad). - Depth-race correction — D276's "occluder" framing was inverted. N64 z/w: smaller = closer. At both confirmed weave points, ~278–310 of 356
0x0c184b50tris are CLOSER than the nearest noise tri. The open question is "why don't the closer textured cards show?", not "does the noise win the depth race?". - Census at the weave points: only 3 real classes reach
(0.05,0.10)/(0.25,0.15):0xc81049d8(IA8 noise painter),0x0c184b50(CI8, CPU-baked bimodal fog 25/255, gfog=0 — the tree candidate),0xc8102078(CI8).0x00552048(z/w=0) and0x00504340(z/w=-1) are probe artifacts.0xc8102078ruled out as trees: KEEPOML run draws 1.4M tris filling the lower half of the screen — terrain. - The crux:
0x0c184b50barely instantiates under deterministic boot.-level_36+ KEEPOML=0c184b50 → ~3–4 tris/frame, zero visible pixels, emptytexdump/; the non--level_36fog run had 720 covering tris. The room-streaming state feeding this class is not pinned by-level_36.
Do NOT re-chase: fog computation for the noise class (verified faithful), 0xc8102078 as a tree layer (terrain), any "trees hidden behind the noise" theory (they're in front).
NEXT STEPS (if the tree thread resumes — multi-step, not a quick win)
- Single instrumented run:
-level_36+GE_D270PIX="0.050;0.100;0.250;0.150"+GE_TEXRAW=1+GE_PCDUMP=1400-1600:1; correlate per tick — does a fog=250x0c184b50tri cover the point AND is the pixel dark? - Log
curRoom/roomsDrawn(D271 infra) alongsideGE_D270PIXto correlate0x0c184b50presence with room-streaming state. - Decode
0x0c184b50's CI8 texture offline — needs a run where it actually uploads (TEXRAW hasn't captured it yet).
Housekeeping / process gaps flagged this pass
- D279 was consumed by an undocumented partial session (Sep 14 ~23:00;
scratch/d279_run.log,d279_imgmap_run.log,d279_g110/g220*.png,d279_gray_1500.png) with no written conclusion — backfilled as a note-only row in findings §F. Next label after it: D280 (used this pass). - D277/D278 probe code (
GE_D277/GE_D278Cinport/fast3d/gfx_pc.cpp) and thescratch/d276_fog_run*.logruns were added by an earlier session and never written up; their conclusions are now subsumed by D280 items 1–2, but the probes remain undocumented in findings.md. - Artifacts this pass:
scratch/d279_collapse_1500.png(noise collapsed — note: mislabelled d279 by the run script),scratch/d279_treeonly_1500.png(KEEPOML tree class, black frame); PPMs inppm/; fog-run logsscratch/d276_fog_run*.log. - Regenerated
docs/dev/findings-index.csvafter the D280/D279 edits. All changes uncommitted per user order.
ARCHIVE — D236 eighth pass (D275/D276), superseded by D280 above
(Header originally mislabelled this "NINTH pass"; findings.md counts it as the eighth.)
TL;DR for a fresh session (you are the reviewer) — read D276 first, then D275, then D274
A second external review of the D274 write-up flagged that pass seven's whole "which class is the wall" framing was never actually verified against the visible pixels — every pass since D269 had reasoned about the CI8 OPA/XLU pair without first confirming those are the classes reaching the pixels the user is actually looking at. Full review preserved in this session's transcript; findings §F D275 (probe work) / D276 (M-133/M-134, what it found).
Method (D275): picked 4 pixel coordinates directly off the reproduced weave screenshot (scratch/d273_base_1500.png, offline, before touching the game), extended GE_D270PIX from one point to a list, added a "kept N tris" counter to GE_D270KEEPOML (was silent — the reason pass seven's isolation attempt was a diagnostic dead end), then ran a 35 s tick-aligned capture (GE_PCDUMP=1300-1700:25) logging every triangle covering each of the 4 points with class, depth, and per-vertex fog.
Finding (D276): the D269-named CI8 classes (0xc4112078 OPA / 0xc41049d8 XLU) don't appear at all at 2 of the 4 confirmed weave pixels. The class that's actually there, front-most, with low fog (so its raw texel shows through largely unwashed): oml=0xc81049d8, fmt=3 (IA), not CI8 — a false-friend that shares the XLU class's low-24-bit oml bits (D272 already had to filter this exact class out of its own budget in an earlier pass without registering what it actually was). Decoded within-run (D274's address-matching method): a fully-opaque, high-frequency blocky gray static/noise texture — nothing like D274's gradient wedge, nothing like a tree. Every instance of this class uses the byte-identical UV window u:0..64, v:0..16 (a 2× wrap of its 32-wide tile) — this is D265's very first "all cards sample the same u:0..64/v:0..16 window" observation, verified to the exact texel. D269's second pass reframed away from that lead onto the CI8 classes without ever confirming the CI8 classes were what D265 had actually been describing.
A third class, oml=0x0c184b50 (CI8), is present at every sampled point with a wildly erratic z/w (-0.0381 to 0.9940) — when it does land in the normal distant range its fog is high (up to 255, plausible "fading into haze" tree behavior). Flagged as the likely real owner of the N64 reference's discrete tree silhouettes, with its own probable occlusion/billboard-orientation bug (not reliably reaching these pixels). D236 may have two separate root causes, not one: (a) the noise texture not being washed toward invisibility the way it should be, and (b) a genuine tree-sprite class failing to draw/occlude where it should.
Do NOT re-chase: the D269 CI8 OPA/XLU pair as "the wall" (D276 — they aren't reaching 2 of 4 confirmed weave pixels; still worth keeping in the picture, just not the primary suspect), the mip-blend-quality-of-a-gradient-wedge theory (D274's leading candidate, now secondary — the newly-identified content is noise, not a smooth wedge, so blend quality is a weaker explanation than it seemed), the ARGB1555 palette swap (D273/D274), seq-based cross-run identity (D274), portal culling (D271).
NEXT STEPS for a fresh session (priority order)
- Fog-correctness check for the
oml=0xc81049d8/fmt=3noise class. D276 measured average fog 39.5–63.3/255 at the two confirmed weave pixels — is that the correct N64 value for this vertex's world position (work out whatfog_mul/fog_offset/z should give), or a computation bug in this specific draw path? This is the cheapest, most direct next test — the data is already logged (scratch/d275_pix_run.log), no new probe needed, just the offline math. - Attribute and understand
0x0c184b50's erraticz/w. ExtendGE_D270PIX(already list-capable) to a couple more points squarely on the N64 reference's visible tree trunks/canopies and see whether this class is the one meant to be there; if so, root-cause why itsz/wswings from-0.04to0.99(billboard basis computed from a bad/momentarily-invalid camera vector is the leading guess, unconfirmed). - Re-run the class-isolation A/B (owed since D274, now with D275's kept-counter and D276's corrected target classes:
0xc81049d8/fmt=3 vs.0x0c184b50, not the D269 CI8 pair) at the tick-aligned window established this pass (GE_PCDUMP=1300-1700:25— all 17 frames confirmed non-black/weave-consistent, so use any tick in that range, not just 1500). - Mip-blend-quality experiment (D274's original next-step) — still worth doing eventually, now lower priority given #1 is cheaper and the content is noise, not a smooth wedge.
- Keep the D271 fix + all probes (D266–D276) until the wall closes.
GE_D273stays default-off.d267_slot_addr(D274) is always-on, keep permanently.
Housekeeping
- Regenerated
docs/dev/findings-index.csvafter the D274 (previously missing its own index row — added this pass), D275, D276 findings.md edits, and the D270 backfill row. docs/dev/GE-ENV-PROBES.mdupdated: new rows forGE_D270PIX(now multi-point),GE_D270KEEPOML/SKIPOML/SKIP/KILLALL(kept-counter),GE_D270FOG, and theGE_TEXRAWcap raise — the whole D270 family was undocumented until this pass.- All changes this pass uncommitted:
port/fast3d/gfx_pc.cpp(GE_D270PIXmulti-point +addr=,GE_D270KEEPOMLkept-counter),docs/dev/findings.md,docs/dev/GE-ENV-PROBES.md,docs/dev/findings-index.csv, this file. Scratch:scratch/d275_*(pixel-attribution run log + 17 tick-aligned frame PNGs, all confirmed non-black at the 4 sample points),scratch/d275_r106_ia8.png(the decoded noise texture — this is the one to look at first). - Kill any running
ge007*before linking/rebuilding.
ARCHIVE — D236 seventh pass (D274), superseded by D275/D276 above
TL;DR for a fresh session (you are the reviewer)
An external review of pass six's D273 write-up caught a real methodology hole and steered this pass (findings §F D274, M-132): all prior passes' "seq" identifiers (D267's d267_slot_img, used by D269/D272/a D270 comment) are stale on a texture-cache hit — the stamp is written after the cache-hit early return in import_texture(), so it can report the identity of some earlier, unrelated upload into that TMEM slot. Confirmed with hard evidence: the same seq value was logged against two different ROM/arena addresses within one 15 s run.
Fix: added d267_slot_addr[128] (port/fast3d/gfx_pc.cpp), stamped unconditionally before the cache-lookup early return, threaded as addr=%p into every D272-family log line. This surfaced a second identity hazard: the runtime address itself isn't stable across separate process runs either. Practical rule: identify and decode a texture within one run, never across runs.
Using that corrected method, this pass closed pass six's open question — D269's "soft gradient blobs" vs. the on-screen herringbone weave turned out to be this session's own offline-script bug (big-endian palette read instead of native little-endian), not a port finding. Re-decoded correctly, the port's existing palette decode produces a plausible gradient wedge matching D269's description. Mip/LOD state confirmed nominal for that tile (genmip=1, tex_lod=1, correct GL_REPEAT).
Superseded by D276: this pass's whole premise (that the D269 CI8 pair is "the wall") turned out to be wrong at 2 of 4 confirmed weave pixels. The gradient-wedge analysis above is still correct as far as it goes — it's just very likely describing a secondary/different layer, not the dominant visible artifact. See D276 above.
D272 (M-130) — still holds
TL;DR for a fresh session (you are the reviewer)
D273 (M-131): the user supplied two screenshots —
docs/img/screenshots/gepd-surface.PNG (1964 N64 emulator, GEPD-edition ROM,
Surface 1 corridor) and docs/img/screenshots/port-surface.PNG (our PC port,
same spot). N64: discrete conifer silhouettes (trunk+canopy visible),
sparse, blending into the sky. PC: a uniform, fine diagonal
herringbone/houndstooth weave, flat-topped, no tree shapes at all —
reproduced headless at -level_36 frame 1500
(scratch/d273_base_1500.png).
While chasing a live reference this session also found an existing,
uncommitted GE_D273 probe already sitting in palette_to_rgba32
(port/fast3d/gfx_pc.cpp) — added by some prior pass, never mentioned in
this file or findings.md before now. It A/Bs the CI4/CI8 palette-entry
decode: standard N64 ARGB1555 vs. the inherited PD-shifted layout, with a
comment theorizing the shift flips an alpha-test outcome for the wall's
palette. Tested it against the reference this session: RULED OUT. Same
frame, GE_D273=1 vs. unset — the wall's herringbone pattern is pixel-eye
identical either way (scratch/d273_fixed_1500.png), while unrelated CI8
content broke (snow path → cyan/magenta moiré, viewmodel hand → teal
skin). Do not enable GE_D273; do not pursue this decode swap further for
D236. Full writeup + the process-gap note (an investigative lead sat
undocumented across a handoff boundary — git diff suspect files, don't
trust the day's HANDOFF narrative as a complete uncommitted-work inventory)
in findings §F D273.
Open discrepancy this reference exposed — RESOLVED by D274, see above. D269's offline CI8 decode of these wall cards described "soft vertical gradients / sparse blobs." What's actually on screen is a sharp, regular herringbone weave. This was flagged as a possible decode/content bug; D274 traced it to a palette-byte-order mistake in this session's own offline analysis script, not a port bug — reconciled, no discrepancy.
D272 (M-130) — still holds
This session reviewed the D271 negative outcome and executed HANDOFF's own next-step #1 (the per-card S/T audit) instead of guessing further. Result, in full in findings §F D272 (M-130):
- Extended the tri-probe family with a new
GE_D272(env-gated, budget 200, filtered to D269's exact two wall-cardomlclasses) that dumps raw per-vertex UV (texel units) + tile fmt/siz/wh/mask/shift for every matching band tri. Built + ran against-level_36(boot camera), 3 iterations to get the filter right (first pass had no oml filter and was swamped by unrelated sky/floor geometry in the same screen band; that unfiltered data is what likely produced D265's "cards all sample the same u:0..64/v:0..16" read — several unrelated degenerate-UV draws in that band really do share one (0,0) UV, but they are not the wall cards). - Both classes confirmed CI8 (
fmt=2 siz=1), matching D269. - UV windows are NOT identical across card instances — small (~0.3–1.6 texel) but real per-vertex jitter between cards sharing the same bound texture. This kills the "converter/tile-setup collapses every card onto one window" theory outright.
- The XLU class's U range is ~143–145 texels — ~4.5× its own tile's
32-texel wrap period. Read
tools_pc/d43_emit.py'semit_main_vtx: the vertex S/T fields go through a plain BE→LE 16-bit byteswap, no scale/ offset/reinterpretation (same pattern as every otherVtxfield) — the wide wrap cannot be a converter bug; it must already be authored in the ROM. This also rules out the D108–D112put_f32-byte-reversal bug class for this specific case (verified by reading the code, not by inference). - Conclusion: the wide horizontal texture wrap is Rare's own design
(tiling one CI8 texture ~4.5× across a long background quad), so the wall
is not a UV-windowing or converter bug. Hypothesis reframed back to
TEXTLOD/mip handling of a heavily-wrapped, heavily-minified tile
(D269's suspect #2) — how OpenGL's
GL_REPEAT+ auto-generated mips render that wrap vs. how the RDP's own per-scanline TMEM wrap + LOD blend would — now ranked ahead of fog range/application and palette.
Do NOT re-chase: "identical UV window across cards" (D272 disproves it
for the real wall classes), a d43_emit.py S/T conversion bug (code-read
verified clean), portal culling (D271, fixed & kept), IA4/IA8 decode, the
blend pipeline, fog combine (all D265–D269, ruled out with evidence).
NEXT STEPS as of pass six (superseded by the SEVENTH-pass NEXT STEPS above; kept for narrative continuity)
Reconcile texture content vs. what D269 decoded— done, see D274 above (resolved: no discrepancy, was an offline-script bug).Minification measurement— done, see D272'sscr0/scr1/scr2+zwfields, already in the live probe.Confirm mip generation actually runs— done, see D274 above (genmip=1,tex_lod=1confirmed).- A second N64 source (
mupen64/) — still nice-to-have, not blocking. - Keep the D271 fix + all probes until the wall closes.
Housekeeping (pass six)
- Regenerated
docs/dev/findings-index.csvviatools_pc/gen_findings_index.pyafter the D272 and D273 findings.md edits. docs/dev/GE-ENV-PROBES.mdupdated with theGE_D272andGE_D273rows.- All changes this pass are uncommitted (
port/fast3d/gfx_pc.cppD272 probe addition —GE_D273in the same file predates this session and was found, not written, here;docs/dev/findings.md,docs/dev/GE-ENV-PROBES.md,docs/dev/findings-index.csv, this file). Scratch artifacts:scratch/d272_run*.log(disposable iteration history),scratch/d273_base_1500.png/scratch/d273_fixed_1500.png(the A/B comparison frames —d273_basematches the user'sport-surface.PNG),docs/img/screenshots/gepd-surface.PNG/port-surface.PNG(the user's reference pair, kept — not disposable). - Kill any running
ge007*before linking/rebuilding.
ARCHIVE — D236 fourth/fifth pass (D271 negative re-test, D272), superseded by D274 above
TL;DR for a fresh session (you are the reviewer)
The prior pass (same day) implemented and verified D271: at Surface 1's
boot camera, 11 portals around room 13 straddle the near plane every frame;
their z==0 clip points project to ±1e20–1e26-scale finite screen coords,
and the old D106 guard (lim=1e5) mapped all of them to full-screen boxes,
force-drawing rooms N64 culls (roomsDrawn 17–18 vs 10–13 post-fix). Fix:
guard lim → 1e38f (non-finite-only); finite garbage now clamps/culls in
bgRectIntersect exactly as on N64. Verified: d106Fallback → 0, room
drops traced per-portal to N64-matching empty clamps, no crashes. Full
write-up: findings §F D271 (M-129).
USER RE-TEST RESULT: NEGATIVE — "it's still not right, trees are a big wall rather than the texture being mapped correctly." So culling was a real N64-divergent over-draw (the fix is kept — it's a fidelity fix, do NOT revert) but it is not the wall's root cause. The user's phrasing points at texture mapping.
State of the hypothesis space (evidence trail D265 → D269 → D271)
- What the wall is (D269): CI8 32×32 billboard cards, two draw classes per room stream (OPA oml=0xc4112078 + XLU oml=0xc41049d8), both BILERP + TEXTLOD-on + perspective-corrected + 2-cycle, omh FOG bit set. Card contents decoded from ROM: soft vertical gradients / sparse blobs (index histogram concentrated at low indices 6–14). NCC of every dumped card vs the captured frame < 0.35 — the cards are not visible as rigid copies.
- Ruled out: IA4/IA8 decode (zero IA4 draws from this camera; the three
IA8 images are noise/mast sprites), the blend pipeline (force-alpha A/B,
D268, proves texel-alpha compositing works end-to-end), fog combine (no
blend cycle in play references
CLR_FOG/A_FOG, so the omh FOG bit is inert), and now portal-culling over-draw (D271 — fixed, wall unchanged). - TOP SUSPECT: per-card S/T / UV windowing. D265 noted "all captured
cards sampled the same u:0..64/v:0..16 window — confirm against N64".
If N64 gives each card its own S/T window into a shared tree-silhouette
texture and the PC samples one region for every card, the result is
exactly a repeating sheet = the reported wall. Mechanism candidates in
order:
- Model DL vertex S/T data — what
tools_pc/d43_emit.pyemits for these cards' S/T (s16 fixed-point; format depends on tile line length). A conversion bug (byte-swap/scale/offset) collapses all windows onto one region. The converter is the usual suspect class here (cf. D108–D112put_f32byte-reversal). - Tile setup in the DL (
gsSPSetTile→ image size, foldS/foldT, tcShift) — wrong tile dimensions for this CI8 surface would map every card's S/T into the same window. - fast3d UV handling in
port/fast3d/gfx_pc.cpp(tex_lod path, GL_REPEAT period, fold math).
- Model DL vertex S/T data — what
- N64 ground truth:
docs/img/bugs/d227-surface-emulator-ref.png— a smooth bright band with sparse organic tree clusters centred x≈0.35–0.75, NOT a continuous sheet.mupen64/(local emulator, Rice plugin) is available for more reference captures if needed. - Side suspect (palette): the archived third-pass noted the port's
palette_to_rgba32reads a=bit0, r=bits15:11, g=bits10:6, b=bits5:1 vs the standard N64 A=15, B=14:10, G=9:5, R=4:0 — verify against the RDP spec; if wrong it affects every CI texture and would turn a tree image into an unrecognizable gradient blob (consistent with D269's decode).
NEXT STEPS for a fresh session (priority order)
- Per-card S/T audit. Extend
GE_D266W(band-gated tri probe ingfx_sp_tri1) to log per-vertex S/T + tile index + image size/line for the wall card classes (oml 0x0c184b50 / 0xc81049d8 family). The deciding question: do distinct cards carry distinct S/T windows, or identical ones? Identical on PC → compare against what the ROM model data says (decompiled DL words) — converter bug if they differ there. Distinct on PC but all landing in the same texture region → tile-setup/fold bug. - Decode the CI8 card + palette to an RGB PNG and LOOK at it
(
scratch/d269_decode.pyexists;GE_TEXDUMP=1 GE_TEXRAW=1now also writes.palfiles). If it's a tree silhouette → S/T windowing owns the bug. If it's a gradient blob → texture data or palette decode is wrong. - Cross-check
d43_emit.py's S/T emission for these cards against the raw ROM model bytes (byte order, fixed-point format). - If PC S/T windows are distinct and sane: fall back to TEXTLOD/mip sampling (does the port generate mips for these tiles? which min filter?) and the palette item above.
- Keep the D271 fix + all probes until the wall closes; then remove the probe sets together (D266–D269 + D271).
Do not re-chase: AC_DITHER/alpha-compare math (D266), IA4/IA8 decode, blend pipeline, fog combine, portal culling (all ruled out with evidence — see findings D265–D271).
Separate, still-open, NOT the same bug: D176(a) — the Surface sky
rendering dark instead of the expected bright horizon band. It has its own
NEXT STEPS list (non-standard combine encoding m1=8, IA16 cloud texture)
in the "third pass" archive below. Do not conflate the two threads.
D271 evidence (pre-fix; reproducible, 3/3 runs)
Probe: src/game/bg.c, small #ifdef PORT additions, all inert unless
GE_D271/GE_D104 are set (uncommitted — git diff src/game/bg.c to see them):
- A
hasPortalsfield added to the existingGE_D104room-visibility probe (~line 656) — confirms Surface 1 does use portal occlusion (not the "no portals, skip culling" branch). - Two file-scope counters near line 269 (
g_d271_ordinary_degenerate_hits,g_d271_d106_fallback_hits), incremented at the two branch points in the portal-bounds degenerate check (~line 1724-1749):ordinary_degenerate= the legitimate min>=max case (a portal genuinely edge-on to the camera — expected, matches N64). Always 0 across every run.d106_fallback= the D106 non-finite/out-of-range guard firing. Never zero, climbs every tick, every run (e.g. run1: 396 hits by tick 601, 781 by tick 1201; run3: 55 by tick 601, 109 by tick 1201).
- A raw-bounds dump (first 12 hits) on the
d106_fallbackpath. Every single hit across the whole capture logs identical numbers:Identical numbers on every hit = the same portal, every frame, from this static boot-camera viewpoint — not a rare edge case, the dominant condition for this viewpoint.min=(2103.126953, -67984331585523982488567808.000000) max=(83783986331400287157223424.000000, 2070298263518753597685760.000000) onscreencount=8min.x=2103is sane; the other three are ~10^24–10^25 magnitude and wildly asymmetric (not "bracketing the view symmetrically" the way the original D106 comment assumed N64 does) — this is theinv_z = -1e20overflow the D106 comment already described,transform3Dto2DWithZScalingbeing fed a z==0 (or z-straddling) portal corner point.
Repro (headless, ~15s, no input needed):
cd /path/to/gh-fullhist
GE_D104=1 GE_D271=1 timeout 15 ./build-pc/ge007.x86_64.exe -level_36 > scratch/d271_verbose.log 2>&1
grep "D104\|D271 d106" scratch/d271_verbose.log
Also noted: roomsDrawn (11–19) and the starting curRoom vary run-to-run
with no input given — almost certainly intro-camera/room-streaming timing
jitter (g_RoomLoadBudget), not the culling bug itself. Don't treat
roomsDrawn alone as a metric; the d106Fallback counter is the decisive
signal, not room count.
Housekeeping
- Commit/land decision for the uncommitted D271 fix + probes (user order is offline-only — nothing pushed). The fix is verified N64-faithful; it can land independently of the wall outcome.
- If regenerating the findings CSV:
python tools_pc/gen_findings_index.py(CI's validate job requiresdocs/dev/findings-index.csvto be current). - An instance may still be running from the user's re-test — kill any
ge007*before linking.
Probe env vars (all inert when unset)
| Var | Purpose |
|---|---|
GE_D104 |
Existing room-render-pass visibility probe (bg.c ~line 656); now also prints hasPortals + the two D271 counters. |
GE_D271 |
Gates the two degenerate-bounds counters and the 12-hit raw-bounds dump (bg.c ~line 1724-1749). |
Files modified this pass (uncommitted, verify with git diff)
src/game/bg.c: the fix — D106 guardlim1e5→1e38 insub_GAME_7F0B5864(~line 1770) + revised comment; probes:hasPortalsfield on theGE_D104print (~line 663),g_d271_ordinary_degenerate_hits/g_d271_d106_fallback_hitsglobals (~line 272), per-point z/projection capture in the bounds builder (~line 1652+), counter increments + raw-bounds/points dump in the degenerate check, and a finite out-of-range (D271 oor-finite) logger.docs/dev/findings.md: D271 index row + M-129 detail section.docs/dev/GE-ENV-PROBES.md:GE_D104/GE_D271rows updated to post-fix state.- Scratch artifacts:
scratch/d271_*.{ppm,log,png}(A/B captures + heat maps; disposable),scratch/d271_1500_sbs.png= the rough visual reference.
ARCHIVE — D236/D265–D269: Surface tree "wall" (third pass, superseded by D271 above)
USER ORDER IN FORCE: offline only — do NOT push anything. All port changes stay uncommitted for user review.
CRITICAL REFRAME (this session's conclusion)
The N64's bright horizon band (~231 luminance) cannot come from the dark CI8 cards we identified. The ROM data is genuinely dark (palette lum mean 77–172, top indices map to near-black). No bright fog color exists anywhere in a 1300-frame run. Our quads render dark. The bright band IS the sky (skyRenderFull/skyRenderTri → skyPortRenderPoly in our PC port), which renders dark in our build due to a texture/S-T/combine issue.
Key findings this session (all env-gated probes, inert when unset)
| Finding | Evidence |
|---|---|
| KILLALL: band → pure black | Band IS tri1 geometry (not clear-color) |
| No single CI8 class explains the dark band | A/B skip meandiffs only 4–11 levels |
| ROM card data is genuinely dark | Both port + standard N64 palette decode give lum 77–172, top indices → near-black (0–42) |
| No bright fog color exists | 71,912 G_SETFOGCOLOR calls over ~1300 frames: all dark blue-gray (96,96,128), (0,0,0), etc. |
| Quad A (oml=0x00552048) IS the sky quad | skyPortRenderPoly sets clip.z=0 by construction; IA16 64×64 cloud tile; re-imported every frame |
| KEEP-A test: band = fog color exactly | pixel(0.15,0.06) = (98,97,128) ≈ fog (96,96,128). Sky renders as pure fog — texture not contributing |
| Quad B (oml=0x00504340, z/w=-1) renders NOTHING | COMPLETELY BLACK in KEEP-B test. N64 RDP clip range is [-w,+w] so z/w=-1 may still rasterize there; our port's near-clip discards it |
| D176(b) = the exact user-visible defect | "tree textures scrambled noise" — geometry exists, silhouette correct, all tree textures wrong |
| m1=8 in sky quad oml is non-standard | No G_CC_* define in gbi.h uses numeric 8. Must be GE-extended encoding. Answer is NOT in gbi.h — it's in gfx_pc.cpp runtime parsing |
Frame 730 draw order at pixel (0.15, 0.06)
[A sky quad] z/w=0 ZMODE_NONE fog=(25,25,25) IA16 64×64 cloud tile
[B1 card] z/w≈0.996 ZMODE_DEC fog=(128,146,148) CI8 16×32
[B2 card] z/w≈0.992 ZMODE_DEC fog=(87,5,15) CI8 16×32
[C1 card] z/w=0.908 ZMODE_DEC fog=(25,25,25) CI8 16×32 ← WINS (nearest)
[C2 card] z/w=0.908 ZMODE_DEC fog=(25,25,25) CI8 16×32
Winner: C card (CI8 0x0c184b50). But the sky quad is drawn FIRST with ZMODE_NONE — it's the background layer. The cards overlay it with depth test.
Why our sky renders dark (the actual bug)
In KEEP-A test (sky quad only), the band shows (98,97,128) = fog color. This means:
- The IA16 cloud texture is contributing zero to the output
- Possible causes: (a) wrong/stale texture import, (b) S/T fold sampling a dark region, (c) combine mode m1=8 producing dark output in our port's GL shader, (d) vFog interpretation issue
NEXT STEPS (priority order)
-
Read
gfx_pc.cppcombine-mode extraction code — find where the port parses oml bits to determine pixel combine operation. The m1 field position/width may differ from standard fast3d. Search:grep -n "combine\|COMBINE\|oml\|OML\|setCombine\|dp_set_combine" port/fast3d/gfx_pc.cpp | head -50. DO NOT grep gbi.h — it's been exhaustively searched; the answer is in runtime code. -
Decode the sky's IA16 cloud texture — find which texdump rNNN corresponds to quad A's 64×64 IA16 import (line=64B, fmt=3). Check if it's bright (haze gradient) or dark. If dark → texture import bug. If bright → combine/S-T issue.
-
Check S/T fold correctness in
skyPortRenderPoly— the cloud tile is 64×64 with GL_REPEAT period=64. Verify foldS/foldT + tcShift produce correct texel sampling. A wrong fold could sample a dark region of an otherwise-bright tile. -
Investigate quad B (z/w=-1) rendering failure — N64 RDP clip range is [-w,+w], so z/w=-1 may still rasterize on N64. If this is a second sky layer, our port's near-clip or projection handling discards it.
-
If mupen64 path is pursued: use
mupencheat.txtto write rmon token at fixed RAM address (find wherermonGetTokenreads from in decompiled code), run with level_36, capture via Rice plugin screenshot. High effort but definitive. -
Check
palette_to_rgba321-bit shift — port reads a=bit0, r=bits15:11, g=bits10:6, b=bits5:1 vs standard N64 A=15, B=14:10, G=9:5, R=4:0. Doesn't change the wall-card conclusion (both dark) but may affect other CI textures.
Environment & build
- Build:
export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8 - Python:
"<python>\python.exe"(has numpy+PIL) - Run: from repo root, ROM in
./data/. Level:-level_36 - Frame capture: F12 →
ppm/frame_NNN.ppm; orGE_PCDUMP=lo-hi:step - Input script:
GE_INPUTSCRIPT="120:START;..."(M-99: CANNOT reach mouselook) - Do NOT run D266W with skip rules — heavy logging + skip causes heartbeat hang
- Probe runs ≤ ~15–20 s (user preference)
Modified files (uncommitted, verify with git diff)
port/fast3d/gfx_pc.cpp: D270PIX probe (~line 1683), skip hooks + KILLALL + SKIPOML + KEEPOML (~line 1655-1760), D266W bbox probe v6 (~line 1932),num_dlsextern + frame counter, fog logging (GE_D270FOG at ~line 2960), tdc cap raised to 600 (line 1181),palette_to_rgba32at line 905port/fast3d/gfx_opengl.cpp: AC-aware edge handling (D266)scratch/d269_decode.py: offline CI8/IA16 decode script
Evidence on disk
ppm/frame_000730.ppm— baseline frame 730 (1957×1468)scratch/base_730.ppm— same frame, copy for analysisscratch/run_skipoml.log,run_keepA.log,run_keepB.log— A/B test logsscratch/run_dump600.log— 1229 texdump files, 436 D267 linesscratch/run_fog.log— 71,912 fog color entriesscratch/run_d270map.log— D270P full run with dl= countertexdump/r*.bin+.palfiles (cap now 600)docs/img/bugs/d227-surface-emulator-ref.png— N64 reference (sky-facing, NOT same camera as frame 730)docs/img/bugs/d227-surface-starburst-ours.png— our broken version (same scene as ref, NCC 0.33–0.41)
Key code locations
port/fast3d/gfx_pc.cpp: D270PIX probe ~line 1683; skip/kill hooks ~1655-1760; D266W bbox ~1932;num_dlsdefined line 3689;gfx_run()line 3697; fog color ~2960;palette_to_rgba32line 905; tdc cap line 1181src/game/sky.c(3050 lines):skyRenderline 275;skyRenderTricallsskyPortRenderPolyat line 2166 under #ifdef PORT;skyRenderFullcalls it at line 2680;skyPortRenderPolyat line 1822src/game/sky.h: declares sky functionsinclude/PR/gbi.h: G_CC_* defines (lines 497-583) — all symbolic, no numeric 8mupen64/: local N64 emulator (Rice video plugin, ROM, Glide64mk2)
Probe env vars (all inert when unset)
| Var | Purpose |
|---|---|
GE_D266W |
Band-gated bbox probe (v6) — logs all tri1 draws covering band region |
GE_D270SKIP=oml24,oml24,... |
Drop draws matching lower-24-bit oml |
GE_D270KILLALL |
Drop ALL tri1 draws |
GE_D270PIX=x,y |
Pixel attribution: barycentric point-in-tri at normalized (x,y) |
GE_D270SKIPOML=oml24,... |
Skip specific oml classes (lower 24 bits) |
GE_D270KEEPOML=oml24,... |
Keep-list: drop everything NOT in list |
GE_D270FOG |
Log every G_SETFOGCOLOR call |
GE_TEXDUMP=1 + GE_TEXRAW=1 |
Dump texture imports to texdump/ (cap 600) |
Oml class frequency (all frames, ~1300 frame run)
| oml24 | fmt | count | identity |
|---|---|---|---|
| 0x00000000 | 3 (IA) | 33 | Unknown (UI/fill?) |
| 0x00504340 | 3 (IA) | 167 | Quad B (z/w=-1, renders nothing) |
| 0x00552048 | 3 (IA) | 790 | Quad A = SKY (z/w=0, 64×64 cloud tile) |
| 0x0c184b50 | 2 (CI8) | 356 | C-cards (nearest, win depth) |
| 0xc8102078 | ? | 34 | D-class |
| 0xc81049d8 | ? | 251 | B-cards |
2026-09-18 session — v0.2.1 hotfix batch: 6 of 7 items done, user-tested (D263 resolved as not-a-bug)
(All v0.2.1 changes remain uncommitted for user review; nothing pushed.)
Batch status (user test pass completed this session)
| Item | Finding | State |
|---|---|---|
| Mouse wheel direction swap | D260 (port/src/input.c inputPostWheel) |
✅ user-confirmed |
| Watch inventory 3D models missing | D264 (src/game/gunfire.c — cross-global ModelRenderData template read zeroed on PC; ABI/layout class, #ifdef PORT explicit template) |
✅ user-confirmed |
| Watch menu WASD/stick sensitivity | D261 (src/game/options.c geWatchStickFastStep() bounded auto-repeat: delay 15 / rate 6 frames) |
✅ works; follow-up logged: W/S still too sensitive — want tactile one-press-one-item, no whole-list scroll on over-held key. Candidate tweaks (NOT done): longer delay, slower rate, or gate repeat to analogue stick only. |
| AllUnlocked sound on fresh save | D259 (port/src/libultra.c geEepromPatchAllCheats seeds music/sfx vol 0xFF for all-zero slots pre-CRC) |
✅ by-ear confirmed this session (fresh-eep launch, jingle plays) |
| Facility Ourumov not shooting Trevelyan | D263 | ✅ RESOLVED: not a port bug. Beat is proximity/LOS-triggered as on N64 (Bond must be walked into Trevelyan's room, lower west wing); full chain incl. execution shot verified firing on PC via temporary teleport probe + user playtest confirmed the sequence plays normally. All probes removed (game files byte-identical to HEAD). Kept: port/src/input.c GE_INPUTSCRIPT stick-token ordering fix (test harness only). Full map in findings D263. |
| Trees/billboards "wall" | D236/D265 | 🅿️ PARKED, reverted. IA4 I3:A1→I2:A2 decode fix was applied and user re-tested: still a wall → git checkout port/fast3d/gfx_pc.cpp (file is back at committed state). Full evidence trail + ranked suspect list in D265 (top suspect now: fast3d's texture_edge/SHADER_OPT_TEXTURE_EDGE path quantizes alpha to opaque/discard, defeating even a correct decode; then per-card UV windowing; then fog). Also verify what N64 actually shows there before more port-side changes. |
| Bing SEO | — (docs/_config.yml site.description shortened; docs/index.md frontmatter trimmed + duplicate h1 removed) |
✅ done, takes effect on next docs deploy (Jekyll not available locally — verified by reasoning over fetched live HTML) |
Uncommitted (user review pending): port/src/input.c, port/src/libultra.c, src/game/gunfire.c, src/game/options.c, docs/_config.yml, docs/index.md, docs/dev/findings.md, docs/dev/GRAPHICS-BACKLOG.md. Untracked scratch/*.png = D265 evidence images (disposable). Next finding label: D266.
D263 prep (SUPERSEDED — D263 resolved this session; kept for the record)
Post-D263 options (user to pick): (1) D261 follow-up — Watch-menu W/S still too sensitive, want tactile one-press-one-item (candidates documented in findings: longer delay / slower rate / gate repeat to analogue stick only); (2) commit/land decision for the uncommitted v0.2.1 batch (user order was offline-only, nothing pushed); (3) D255 sign-off — NULL-model guard in vtxstore.c (rule #2, diagnosis pinned); (4) rebase + land PR #56 (D231 mute-on-focus-loss + F12 productization); (5) parked cosmetics: D236/D265 tree wall (top suspect now fast3d texture_edge alpha quantization — also verify what N64 shows there), D243 Dam level-end race, D245 water seam, D246 edge strips, D75 logos, D251 F10 overlay dup; (6) parked proposals: UNLOCKED-FPS-PLAN, WIDESCREEN-FOV-PLAN, QOL-INVENTORY.
Facility = -level_34 (stageId 34 = LEVELID_FACILITY; boss.c decodes -level_XX as d0*10+d1−0x210; table in port/src/main.c). The scripted opening beat (Ourumov + squad; on N64 Ourumov shoots Trevelyan) never triggers. "ouromov" is ROM asset data, not a source symbol — static chase is out; this is a runtime probe session.
Map already located (saves the next session the search):
- Stage setup blob for Facility =
UsetuparkZ(setup_text_pointers[34],src/game/chraidata.c:817; names are NOT level-name-derived — don't look for "fac"). Loaded atsrc/game/prop.c:1271via_fileNameLoadToBank("UsetuparkZ", …)intog_ptrStageSetupFile; internal byte-offsets rebased onto the RAM copy intog_CurrentSetupat prop.c:1280+. - The beat itself =
g_CurrentSetup.intro— a tagged record array (stagesetup.intro,src/bondtypes.h:4144). Record types:SetupIntroEmpty/Spawn/Item/Ammo/Swirl/Anim/Cuff/Cameraat bondtypes.h:3849–3965 (tagged union-ish;SetupIntroCameracarries a linkedprev). Consumed insrc/game/bondview_r.c(init, ~line 115) andsrc/game/bondview2.c(per-frame/respawn, ~8749). - Suspects per D263 triage: (1) D193 family — NPC beat timing/trigger volumes under port tick pacing; (2) beat start condition needing Bond still / specific camera mode (intro-handling changes); (3) silent anim-name lookup failure (low priority — user says never triggers, not triggers invisibly). Known-good precondition: D253 (crouch-tile SEGV) fixed, so Bond reaches the beat.
First moves (next session):
- Preflight per the block above; read D263 + D193 + D203/D253 in findings via the index; skim
docs/dev/GE-ENV-PROBES.mdfor probe conventions. - Repro:
-level_34, wait out the intro, stand at the opening crouch tile, watch ~60 s. Headless is fine for the first pass (GE_INPUTSCRIPT; note ~2 controller reads per render frame — time scripts empirically, see D261's pulse-train notes). - Instrument: env-gated probe logging the intro-record state machine (which record index is active, its tag, and the beat's trigger/sequence counters) each tick for the first ~60 s; compare against what
UsetuparkZ's intro array actually contains (dump the rebase'd records once at load — offsets are relative to the blob start). - Diff behaviour vs a level whose opening beat DOES work (e.g. Dam
-level_33) to isolate "Facility data" vs "general beat machinery". - Any fix: port-only unless it's the documented ABI/layout class; new finding = D266+; append to
docs/porting-notes.mdif a new bug class emerges.
Environment (unchanged from prior sessions): build export PATH="/c/msys64/mingw64/bin:$PATH" && cmake --build build-pc -j 8 (or ./build-pc.sh ntsc-final); kill any running ge007* before linking. Exe build-pc/ge007.x86_64.exe, run from repo root (ROM in ./data/). Headless: GE_INPUTSCRIPT (tokens A/B/Z/START/UP/DOWN/LEFT/RIGHT/CUP/CDOWN/CLEFT/CRIGHT/SDOWN/SNONE, tick:TOKEN;…, pulse = 6 reads), GE_PCDUMP=lo-hi:step, GE_INPUTLOG=1, F12 → ppm/shot_NNN.ppm. Python (PIL/numpy) at <python>; cast uint8 arrays to int32 before bit shifts.
2026-09-17 session — v0.2.0 PUBLISHED (tag e1cb96ef, live 2026-09-14 04:32 UTC; user flipped draft→public themselves)
Final pre-publish commit e1cb96ef (on top of the docs pass 436484a9):
AllUnlocked default OFF (fresh-save audio quirk, new finding D259; F10
toggle unchanged), draw/LOD distance defaults 100→150%, MSAA default off→4×.
v0.2.0-pre draft release deleted; v0.1.0 kept. User re-tested and published.
Next (post-release backlog, in order):
- D255 sign-off — NULL-model guard in vtxstore.c (rule #2, needs user sign-off; diagnosis pinned). A fresh Deck crash log arrived during the publish window (old v0.2.0-pre bundle; user then said "user error or something" — unconfirmed, but consistent with D255's combat repro).
- D259 fix (small, port-only): seed
music_vol/sfx_vol = 0xFFfor all-zero slots ingeEepromPatchAllCheatsbefore CRC recompute (~6 lines, libultra.c) so AllUnlocked-on with a fresh save doesn't load volume 0. - Rebase + land PR #56 (D231 mute-on-focus-loss + F12 productization; branch orphaned by the history rewrite — rebase onto new main).
- D243/D245/D246/D75/D251 per findings index → issue #85 (PAL/JP) when picked up.
.github/release-notes.mdstill holds the v0.2.0 body — rewrite for the next cycle before the next tag (CI only re-stamps<version>).
2026-09-16 session — D230 bass fix (PR #83) + drop-in ROM Part A (PR #84); BOTH MERGED, user playtested; only the v0.2.0 tag+publish remains
State: main = b2d5551e (#84 merge; #83 = 99bc2a3a). ALL pre-tag work
is done. User built + playtested the merged main locally after the merges —
test passed, no further defects reported. The tag flow in "Next steps" below is
the only thing left for v0.2.0.
What landed:
- D230 fix (PR #83, merged): the bass "organ" timbre on Control/Cavern/
Runway was a byte-order bug — RAW16 wavetable samples bswap'd per-sample in
romdataFinishCartMap(port/src/romdata.c, D55 in-place-fixup idiom). - Drop-in ROM Part A (PR #84, merged): issue #6 Part A + interim —
drop-in NTSC-U ROM, no Python. Engine-side spawner
port/src/romconvert.c(romConvertEnsureSidecars, called fromromdataInitafter ROM validation): missing sidecars → spawn<exedir>/prepare-assets/ge007-convert[.exe]with--rom … --out …, wait, re-check; failure aborts boot with an actionable message (no more D179-class crash).prepare-assets.pyruns its three emit scripts viarunpywhen frozen (two real bugs fixed:run_pathdoes NOT setsys.path[0]— d88'simport d88_propdefsdied; tuple-assignment syntax error); NTSC-U-only gate after SHA-1 with clear PAL/JP message. CI freezes the converter (PyInstaller--onefile) in both linux + windows jobs before bundling; bundle scripts copy it intoprepare-assets/(hard-fail if missing) and raise the size guard 60→120 MB. Release notes updated: US-only drop-in, no Python. Verified locally: frozen exe output byte-identical to unfrozen path and to shipped sidecars;./build-pc.sh ntsc-finalclean. First CI run failed twice, both fixed before merge: (a) windows-build — MSYS2's MinGW Python has ensurepip disabled, so the pip bootstrap died → installmingw-w64-x86_64-python-pipvia pacman; (b) validate — D258 row madedocs/dev/findings-index.csvstale → regenerated withpython tools_pc/gen_findings_index.py. Final CI run all green. - Issue #85 filed = Part B follow-up: PAL/JP filelist CSV repair (31
truncated model rows in
filelist.e.csv+ 1 mislabeled JP font row — D258, repair path verified but NOT applied), per-region exes + re-exec guard, multi-region CI, optional C port of the emitters. - D258 added to findings (PAL/JP conversion broken at source-data level; no PAL/JP PC build has ever been producible from this tree).
Next steps for a fresh session, in order:
- v0.2.0 tag flow (the ONLY remaining pre-publish work): user is ready →
git tag v0.2.0 && git push origin v0.2.0fromb2d5551e→ CI cuts both bundles (with the frozen converter) + re-stamps.github/release-notes.md(<version>→0.2.0) → verify: CI green, assetsgoldeneye-pc-port-0.2.0-{win64.zip,linux-x86_64.tar.gz}+ sha256s, notes body final + pinned GIF URL (raw/v0.2.0/…) resolves → explicit user approval → publish → delete thev0.2.0-predraft (keep v0.1.0). HARD RULE: never publish without explicit approval; CI only updates the DRAFT. Optional pre-tag smoke:./build-pc/ge007.x86_64.exe --versionshould show codenamelarkspur+ origin line (#82 feature; the user's post-merge playtest already covered the game itself). - Post-release backlog: D255 sign-off (NULL-model guard in vtxstore.c — rule #2, needs user sign-off; diagnosis pinned, see D255) → rebase + land PR #56 (D231 mute-on-focus-loss + F12 productization; branch orphaned by the history rewrite — rebase onto new main) → D243/D245/D246/D75/D251 per findings index → issue #85 (Part B: PAL/JP + C emitters) when picked up.
- Housekeeping: strike this section's done state after publish.
Environment gotchas found this session (MSYS2 MinGW Python 3.14):
python -m pipabsent ANDensurepipdisabled →pacman -S --noconfirm mingw-w64-x86_64-python-pip, thenpython -m pip install --break-system-packages …(PEP 668 externally-managed). Same on the CI windows runner.- PyInstaller freeze:
python -m PyInstaller --onefile --noconfirm --clean --name ge007-convert --distpath tools_pc/dist/prepare-assets --workpath build-pyinst --specpath build-pyinst tools_pc/dist/prepare-assets/prepare-assets.py— needsHOME+USERPROFILEset (MSYS2 shells often lack them). Output ~9.6 MB exe; both are gitignored (tools_pc/dist/prepare-assets/ge007-convert*,build-pyinst/). runpy.run_pathdoes NOT prepend the script dir tosys.path(onlypython script.pydoes) — sibling imports need an explicit insert; MSYS2 paths carry Windows backslashes, normalize for sys.path entries.- User WIP in working tree — do NOT commit these:
README.md,docs/index.md,tools_pc/dist/README.md.in(style pass, still uncommitted at session end).
2026-09-15 session — v0.2.0: everything merged, tag + publish are the only steps left
State: main = 2c95fcd1 (post-rewrite hash — see history note below). ALL
pre-tag work is done and merged: #77/#78/#79, then #80 (docs/media pass),
#81 (release notes final), #82 (build provenance: --version prints codename
larkspur + origin URL; fork policy in CONTRIBUTING). User completed a full
campaign playtest — all 21 missions, Agent difficulty, no crashes — so the
README/Pages now carry the end-to-end-completable claim (playtested), D254 is
field-verified FIXED, and D255 got its register capture: Rdi=nil → the
diagnosis is pinned to a NULL ObjectRecord.model on an active record
(destroy-path ordering; repro Bunker + Frigate during combat). Truncation was
already ruled out. Media finalized: the mp4 montage is gone from the tree AND
from history (filter-repo purge); the 28 MB GIF is the single montage asset in
README hero + Pages + release notes; old Sept-1 attract PNGs replaced with the
Sept-13 playtest gallery shots (shot-06 Bunker, shot-01 Dam); three orphan bug
screenshots deleted. Release notes had the "mute-on-focus-loss" claim DROPPED
(D231 is NOT on main — it ships post-release via #56; F12 screenshot IS in
main and stays claimed).
History note (2026-09-15): git filter-repo purged the mp4 from history.
~289 of 666 commits got new hashes (deeper than expected — the repo had been
rewritten before, and filter-repo's message cleanup touched older messages too,
e.g. stripped one (#NN) suffix). Tip tree verified byte-identical; tags
v0.1.0 + v0.2.0-pre re-pushed at new targets; releases/assets untouched.
Old SHAs cited elsewhere in this file are pre-rewrite — GitHub redirects
them, but use git log for current ones. Undo is still possible (old objects
unpruned locally: main 3b9e704d, v0.2.0-pre tag obj 70702aeb, v0.1.0
7aba3637) but the user decided to keep the lean history.
Remaining to ship (in order; HARD RULE: never publish without explicit user approval — CI only ever creates/updates the DRAFT):
- Local smoke: build + launch, confirm
--versionshowslarkspur+ origin line and the startup log carries both (#82 feature, unverified this session). git tag v0.2.0 && git push origin v0.2.0→ CI builds both bundles, creates/updates the draft release, stamps<version>→0.2.0in the notes.- Verify: CI green; assets
goldeneye-pc-port-0.2.0-{win64.zip, linux-x86_64.tar.gz}+ sha256s; body shows final notes and the pinned GIF URL (raw/v0.2.0/…) now resolves. - User reviews the draft → explicit approval → publish.
- Delete the
v0.2.0-predraft release after the final is live (keep v0.1.0). - Housekeeping: strike this section's done state, then post-release backlog:
D255 sign-off (fix = NULL-model guard in
vtxstore.cvtxstore_fix_refs— rule #2 game-code change, needs user sign-off; diagnosis is pinned, see D255) → rebase + land PR #56 (D231 mute-on-focus-loss + F12 productization; its branch was orphaned by the history rewrite — rebase onto new main, don't expect a clean merge-base) → D243/D230/D245/D246/D75/D251 per the findings index.
2026-09-14 session — v0.2.0 finalization (SUPERSEDED by the section above; PRs #80/#81/#82 merged, tag still pending)
State: main = 442afa58 (#77/#78/#79 all merged; #79's conflict was the
optionsoverlay row table — resolved by keeping all three new rows: NoHitFlash,
SkipIntro, AllUnlocked). Three PRs now await user merge, all must land
before the v0.2.0 tag:
- PR #80 (
docs/preview-montage): the two montage commits (32 s mp4 in README hero + Pages site, preview-video runbook; demo-gif pruned) + finalization pass: "v0.2.0 (pre-release)" → v0.2.0 everywhere; stale "not yet verified on real Deck hardware" line replaced with the playtest fact (D253 fixed / D255 open); known-issues lists aligned 1:1 with release notes incl. finding labels; AllUnlocked in the Working list; first-public- release note (hedged "to our knowledge") in README hero + site; newsocial-preview.png(1280×640 link card, composed from a montage frame via localscratchpad/make-social-preview.py);manifest.csvcommitted,CONTACT-SHEET.jpggitignored,scratch-gdb-cmds.txtdeleted. User must eyeball the social-preview card — this environment's model can't view images. - PR #81 (
docs/release-notes-020):.github/release-notes.mdfinalized for final (title, blockquote rewritten honestly — "load and run crash-free", NO end-to-end-completable claim yet; new bullets: Everything-unlocked-by- default D257 + Deck first-run preset/NoHitFlash/SkipIntro; known issues unchanged — all eight confirmed still open). Preview GIF at top of body, raw URL pinned to thev0.2.0tag → 404s until #80 merges AND the tag exists. If the user does a full 21-mission campaign run before tagging, say so and upgrade the blockquote to the completable claim. - PR #82 (
feat/build-attribution):GE007_VERSION_CODENAME("larkspur")GE007_ORIGIN_URLin versioninfo →--versionprintsbuild : <hash> (larkspur)+origin : https://github.com/jkdansereau/goldeneye-pc-port, startup log carries both. CONTRIBUTING gains a "Forks and derivative works" section (rebrand/localize/extend welcome; attribution + preserve-or-linkdocs/dev/are the ask; stripping attribution disqualifies). Build verified ntsc-final.
Fork intel — PRIVATE, never in tracked files or PR bodies (user order):
MukaSanches/goldeneye-ascension (+ site mukasanches.github.io/ goldeneye-ascension) is a fork of THIS repo created 2026-09-12 (pre-
v0.2.0-pre), rebranded PT-BR "Ascension": own logo/roadmap/changelog + ~20
AI-generated "learning series" course PDFs linked from its README; presents
itself as an independent line while preserving our credits for now. User's
read: low-effort bad-faith AI spam to deter. Strategy shipped in #82 (nothing
hostile): every build from our tree self-identifies (origin URL + codename
larkspur); when/iff they sync to pick up v0.2.0 (first cut with 60 fps +
audio + crash fixes — the only sane base) they inherit it. Detection if they
later claim independent authorship: search their tree for larkspur /
GE007_ORIGIN_URL / the origin URL. Do not engage publicly unless the user
asks.
Remaining to ship (order): user merges #80 → #81 → #82 → cut tag
v0.2.0 (CI builds both bundles + re-stamps release body from the notes
file) → verify CI green, asset names goldeneye-pc-port-0.2.0-*, sha256s,
body shows final notes + GIF resolves → user publishes/flips to final
(HARD RULE: never publish without explicit approval). Also user's call:
what happens to the v0.2.0-pre draft (suggest: delete after final is
published, keep v0.1.0 as history). Then Task C = D236 billboard diagnosis
(half-day budget, stop-and-report; next finding label D258).
2026-09-13/14 session — post-gate feature queue (Tasks 0–2 done as PRs; Task 3 gated on publish state; Task 4 next)
Per docs/dev/notes/NEXT-SESSION-PROMPT.md's 5-task queue. Own branch + PR per item; user merges.
- Task 0 — DONE, PR #77 open (
feat/deck-first-run-preset): Steam Deck first-run preset (the section further down describes it). Caveats: existing-ini Decks need-fresh/F10; 16:10 stretch. Real-Deck test owed from the CI artifact. - Task 1 — DONE, PR #78 open (
feat/f10-game-toggles):Game.NoHitFlash(D232) +Game.SkipIntro(D216) as ROW_TOGGLE rows after Screen shake. Deliberately excludedVideo.FixMipTextures/Video.WrapFix(opt-in fidelity bug-fixes, not preferences). User-verified live: rows render/toggle, persist to ini, SkipIntro=1 boots to file-select. - Task 2 — DONE, PR #79 merged (
feat/all-unlocked@f49331c1):Game.AllUnlockedoption (default flipped OFF for the v0.2.0 final: with it ON a fresh save loads audio at volume 0, D259; F10 toggle unchanged). All levels at all difficulties + 007 mode via the two liveLEFTOVERDEBUGRAM flags seeded inmain.c; eep-shim read-time patch sets all 20 cheat-unlock bits and fills empty completion times (0x3FF) with per-slotfileGenerateCRCrecompute — the completion half is required or enabling any cheat locks every level in mission select (g_AppendCheatSinglePlayer→ COMPLETED-only; user repro "could only do Silo"). User playtest passed (time-modifying cheats untested). Full mechanism + thetimes[80]-vs-76 mirror pitfall: findings D257. - Session end (release plan agreed with user): all eight known issues in
.github/release-notes.mdwalked one by one — all confirmed still open (wording-only changes for final; nothing removed). Plan for next session, in order: (1) user merges #77/#78/#79; (2) Task A README/pages + media pass (docs-only PR; commit referenced media, deletescratch-gdb-cmds.txt; user says pull new images from the contact-sheet montage) — must land before the tag because bundles ship their own README.md; (3) Task B finalize.github/release-notes.mdfor v0.2.0 final (<version>→0.2.0, drop pre-release, rewrite blockquote, new What's-new bullets incl. explicit AllUnlocked-default-ON bullet; "all 21 missions completable" becomes a claim only if the user does a full campaign run); (4) tagv0.2.0→ CI re-cuts assets + release body → user publishes/flips to final (hard rule: never publish without explicit approval); (5) Task C = D236 billboard diagnosis (half-day budget, stop-and-report). Full detail:docs/dev/notes/NEXT-SESSION-PROMPT.md(rewritten for the fresh session).
D255 stays open with its decision procedure in the section below. Next finding label: D258.
v0.2.0-pre: at the publish gate (2026-09-13 evening) — D253/D254/D256 fixed, D255 narrowed; user publishes the draft when ready
HARD RULE (user-stated): do NOT publish/deploy the release without explicit user approval. CI only ever creates/updates the draft; publishing is manual on the GitHub Releases page (or gh release publish only if the user explicitly asks).
State: tag v0.2.0-pre = 1e88fea0 (notes-only bump over 8d4e7443, the code state). Draft release live with 4 assets + sha256s; CI re-cut of 1e88fea0 was in progress at session end (two push runs, ~10 min) — verify both green and that the draft body shows the final notes (D256 bullet, updated D255 line) before telling the user it's ready. The code in the assets is byte-identical to what the user already playtested on Windows + Deck.
Session work (all committed/pushed on main):
c1b8560cD253 FIXED (ABI-class):stanCheckLinkedSpecialTileprototype float/s32 mismatch — SysV x86-64 ABI divergence, N64 codegen unaffected. Deck-verified by user (got past the crouch tile).ce59e856D254 FIXED: Linux crash handler re-entered on SIGABRT and truncated its own log. Now: re-entrancy guard,_exit(128+sig), FAULT ADDR (si_addr) + full register dump (Rax/Rcx/Rdx/Rsi/Rdi/R8–R11/Rsp/Rbp) from ucontext inge007.crash.log. Note the log is written to CWD, not exe dir — a launch from an unwritable CWD loses it.8d4e7443D256 FIXED + Deck-verified by user ("it fixed the save on steam deck"): POSIX$Sinport/src/system.c sysResolvePath()now mirrors the Windows branch — CWDdata/else exe-dirdata/viareadlink(/proc/self/exe), best-effort mkdir. This also explains the Deck "draw distance is low" report: config never persisted, every launch started at defaults (DrawDistance 100% = faithful N64 far-clip).1e88fea0release notes: D256 bullet added, D255 line updated, "no known crashes" claim dropped from the blurb.
D255 (OPEN, intermittent) — current understanding: SIGSEGV in sub_GAME_7F09BAC4 (vtxstore.c, PD name vtxstore_fix_refs) during heavy firefights on Deck. Called from vtxstore_tick (7F09BBBC) whenever vertex batches merge; walks active PropRecords with type==1 (PROP_TYPE_OBJ), reads ObjectRecord.model (offset 0x14) via a 32-bit zero-extended load (decompiler mislabels the union member as ChrRecord.chrflags), then derefs model->obj. Truncation is RULED OUT: all game memory (mempool banks incl. MEMPOOL_STAGE where Models/records live) sits in the emulated N64 DRAM window 0x70000000–0x70800000 (< 4 GB; tlbmanageGetTlbAllocatedBlock → 0x70700000, n64stubs.c). Remaining mechanism: stale/NULL model pointer on an active OBJ record (destroy-path ordering — crate/effect dies, record lingers or its memory is reused when the merge tick lands in the window). objInit only sets model when non-NULL; the NULL-model path found (chr.c:1890) is CHR-only (type 3, skipped by the walk). Next Deck crash's ge007.crash.log decides: FAULT ADDR == 0x10 → NULL model; other small/garbage → reused record memory; Rax = the ObjectRecord. Fallback if ambiguous: env-gated probe in vtxstore_tick dumping each active OBJ's model pointer per merge (GE_D51/GE_D250 precedent). User has since played Facility on Deck without crashing — it's intermittent; tell them to grab the log before the next crash overwrites it. Any level with object churn works, not just Facility.
Deck testing facts: local WSL builds are NOT Deck-compatible (glibc 2.43 vs SteamOS ~2.38) — only CI artifacts (ubuntu-24.04) ship to Deck; WSL is for debugging only. User's Deck flow: SFTP the tarball from the draft release, unpack, data/ with ROM + generated sidecars (prepare-assets), non-Steam game shortcut. Sidecar gotcha found while testing: a data/ without pccg-*/+pcmodels-*/ starts fine but faults deterministically in load_bg_file (bg.c:865) on level load — expected degraded mode (reserve fns warn "stage loads will fault"), not a bug; don't chase it.
Tree hygiene: untracked in repo root/docs: docs/img/shots/CONTACT-SHEET.jpg, docs/img/shots/manifest.csv, docs/media/goldeneye-*.gif/.mp4 (playtest media from the gallery pass) — decide keep-vs-gitignore next session. scratch-gdb-cmds.txt at repo root is throwaway.
After publish: D255 register capture (above); then post-release backlog per findings index (D252 rainbow particles — user said document-only for this release; F10 bottom-row duplicate; D243/D230/D245/D246/D75).
Post-gate local branch feat/deck-first-run-preset (unpushed, "final v0.2.0 touches"): Steam Deck first-run preset + better default view distance, per user request. getenv("STEAMOS") in main.c → videoApplySteamOSDefaults() (port/src/video.c) before configLoad(): Fullscreen=1, Window 1280×800 (Deck native), VSync=1, MSAA=4, DrawDistance=150, LodDistance=150. First-launch-only by construction — no ini → configLoad's first-run path saves the preset; existing ini always wins (verified: user values 220/MSAA 2 survive a STEAMOS relaunch; note Video.DrawDistance clamps to min 100, pre-existing). Verified on Windows with STEAMOS=1 from an isolated CWD: first-run ini seeded correctly, window opened 1280×800, repo's live data/ge007.ini untouched. Caveats logged in code + README Deck section: 16:10 panel stretches the 4:3 render (letterbox = parked WIDESCREEN-FOV-PLAN); a Deck that already has an ini (user's playtest one) needs -fresh or F10 to pick the preset up. Draw-distance context for item 2: default 100% is N64-faithful authored fog/fade (the Dam lock fade-in is console behaviour); Video.DrawDistance scales far fog + prop/char fade cutoffs (bgfog.c, propobj.c), Video.LodDistance scales LOD-swap distance (model.c) — both F10-settable and persistent since D256.
v0.2.0 pre-release cut (superseded by the section above, 2026-09-13) — docs + release artifacts; Steam Deck was the final blocker
Task: cut the v0.2.0 pre-release (Windows + Linux/Steam Deck). User
will do a full manual playtest in lieu of the verify.sh sweep.
Done this session (all committed on main):
f3882040D194 closure recorded (M-123);436e7c82D251 F10 overlay QoL pass (ships WITH the intermittent 4K-fullscreen ghost-row bug — user decision, logged as known issue).4f6092ddlegacy always-grab mouse mode removed entirely (D192 + D239 closed):Input.MouseCaptureModegone frominput.c/input.h/optionsoverlay.c; click-to-lock is the only capture mode.- Docs: ROADMAP-1.0.md got a "Task 1 status addendum (v0.2.0 pre-release cut)"; README Status rewritten + Download section now leads with the two v0.2.0-pre bundles (asset names follow
bundle-*.sh: leadingvis stripped, sogoldeneye-pc-port-0.2.0-pre-{win64.zip,linux-x86_64.tar.gz}) + new Steam Deck subsection.
Remaining before tag:
tools_pc/bundle-linux.sh: bundle SDL2.so*into the tarball with an$ORIGINrpath (mirror ofbundle-win.sh's DLL allowlist) so the Deck sideload is turnkey — README already claims this; make it true.- Build both bundles from the tag commit:
tools_pc/bundle-win.sh v0.2.0-pre,tools_pc/bundle-linux.sh v0.2.0-pre(Linux bundle: cross-check on a real Linux box or CI artifact). - Steam Deck test session (~1 hr, needs the hardware — who has the Deck?): install tarball via USB, Facility walk+fire (D203 repro profile), gamepad check,
Video.DisplayFPSon Dam + Streets, grabge007.crash.logif anything faults. - Tag
v0.2.0-pre; publish pre-release with known issues: D243 (Dam end race), D230 (music quality), D245 (water seam), D246 (edge strips), D197 (face wrap on Silo), D75 (front-end models), the D251 4K ghost row, and "not yet verified on real Deck hardware". Flip to final after the user's playtest passes.
Environment: build as before (/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd <repo> && ./build-pc.sh ntsc-final'). CI (.github/workflows/ci.yml) builds Linux per push — the linux-build job's artifact is the bundle source for the tarball if local Linux testing isn't available.
F10 overlay QoL session (2026-09-13, shipped in v0.2.0-pre) — D251; known bug: intermittent 4K-fullscreen "repeat of top entry" at panel bottom (MSAA×fullscreen driver suspect, unverified)
Task: QoL pass on the F10 in-game options overlay (port/src/optionsoverlay.c, port-layer only). User is playtesting live; an instance I launched was PID 11508 (may be dead by now — just relaunch from repo root, see commands at bottom of this file).
Done + verified building (D251 in findings.md):
- Scrollable panel:
maxVisibleRows()/s_visIdx[]/s_scroll; 17 rows no longer overflow the 240-tall in-game space.s_selis a visible-list index, not a row index. - MSAA cycle wraps both directions:
idx = (idx + dir + 4) % 4. - Auto-coupled % rows (Draw distance %, LOD distance %) hidden while their auto toggle is ON (
hiddenIfOn→ resolved tohidePtrat init). - Nav (wheel + arrows) clamps at both ends (no wrap). Sliders clamp; only discrete cyclic rows (MSAA/enum/res/toggles) wrap.
- Hover-to-highlight removed entirely — mouse movement never moves selection or scroll; only click / wheel / arrows do. (The old hover→select→scroll-shift feedback loop near list edges was a likely source of the "duplicate row" twitch.)
- Sensitivity link:
Input.SensLinkconfig key (default ON) inport/src/input.c;rowSet()scales AimModeSens=38 : MouseTurnSpeed=50 bidirectionally with ans_linkDepthre-entrancy guard. Overlay-side only (direct ini edits bypass it — by design). - "Mouse aim speed" row repointed from inert
Input.MouseAimSpeedto the active GEPD knobInput.AimModeSens(uiMin 1, uiMax 80). New "Link aim/turn sens" toggle row added.
OPEN BUG — REASSESSED (wrap theory dead, local repro clean): launching at 4K (user's monitor 3072×1728; live ini Fullscreen = 1, MSAA = 4), opening F10 shows a dark, unselectable repeat of the top entry ("Fullscreen") at the bottom of/under the panel. Only in that mode.
Reassessment (fresh trace, this session):
gfxFramebufferhas zero readers — the textured-quad present (G_SETTIMG_FB_EXT/select_texture_fb) is never used by the game. Presentation is a full-frameglBlitFramebufferfrom the same-size MSAA FBO to the default framebuffer (fb index 0 is reserved for the window;different_sizeis structurally always false since both dims come fromgfx_current_window_dimensions). No UVs in a blit → UV-wrap is impossible; that theory is dead. Do not apply the CLAMP_TO_EDGE fix — it would change nothing.- The overlay DL draws each visible row exactly once, in-bounds, own scissor (0,0,W,H).
aspect_modeis never set by GE (noG_ASPECT*anywhere in src/), so 16:9 just stretches RATIO_X≠RATIO_Y uniformly through viewport+scissor — no letterbox offset, mouse mapping stays correct. - Local repro attempted and CLEAN: windowed-maximized 16:9 (2987×1705) + MSAA=4 +
GE_OPTIONSOVERLAY=1+GE_PCDUMPon-level_09→ no ghost row. The FBO/MSAA path alone is not the trigger. - Remaining variable: true SDL fullscreen on the 3072×1728 display (driver/display-level suspect — that resolution is non-standard; check Windows DPI scaling vs actual drawable size if it persists with MSAA off). Also removed: the
GE_OV_FORCE_SELtemp debug hook (done this session).
User update (logged, NOT fixed yet): the ghost is intermittent across launches; user suspects the Draw/LOD distance % rows appearing/vanishing when their auto toggles flip (hiddenIfOn visible-list resize). Unverified — emit and input both rebuild s_visIdx+scroll together each frame, so if it's this, the window is the transition frame itself. User: so far it appears on first open of F10 (not only after an auto-toggle flip). Next (needs the user, ~1 min): bisection on their machine — F10 → set MSAA = OFF (tagged restart) → restart, still fullscreen 4K → open F10: ghost gone ⇒ MSAA×fullscreen driver interaction (park as display-specific, maybe try a standard 2560×1440 fullscreen); ghost persists ⇒ pure-fullscreen path bug, then instrument gfx_current_window_dimensions vs SDL_GL_GetDrawableSize at frame start in fullscreen. Also logged (QoL follow-up): make the auto-linked % rows visually subordinate to their toggle (indent / dimmer colour) so the pairing reads obviously.
Environment / state notes:
- User's live
data/ge007.inialready hasFullscreen = 1,MSAA = 4(backup at/tmp/ge007.ini.bak— I did not modify their ini). Monitor: single display 3072×1728, window fullscreen at 0,0. ppm/user4k.pngexists from a PowerShellCopyFromScreencapture but was taken with the overlay closed — low value; user asked to stop analyzing captures.- Build:
/c/msys64/usr/bin/bash.exe -lc 'export PATH=/c/msys64/mingw64/bin:$PATH; cd <repo> && ./build-pc.sh ntsc-final'. Launch from repo root:./build-pc/ge007.x86_64.exe &(needs./data/ROM). - Design doc for the overlay:
docs/dev/OPTIONS-MENU-PLAN.md(approach C: port-layer overlay, zero src/ menu edits).
M-123 (2026-09-14) — D194 ① log mined + wrap-spike fixed; spazz NOT in our writes (they're rock-stable); next: live capture with the new residue/autoaim logging, then user A/B playtest
Worked ① (crosshair/arm spazz) per M-122's plan.
Log mining (d194_gepd.err, 1964 gepdaim lines) — key result: the port's own writes are rock-stable. Zero crosshair/camera jumps while aim is held (scan: consecutive gepdaim lines with btn&0x2000, Δcam>3° or Δcross>0.5 → 0 events). Every large camera delta in the log turned out to be user hipfire stick swings between polls (verified against the interleaved cont0 stick lines — e.g. the ±20–38° "jumps" at gepdaim indices 1183/1348 follow stick ramps to (80,26)). Exactly ONE true anomaly: a +359° one-tick spike in raw vv_theta (cam 0.5 → 359.7 at log line 534) — our own [0,360) wrap of the edge-scroll accumulator firing when a leftward scroll crossed zero. Fixed: the two wrap lines are deleted (port/src/input.c aimGepdCompute) — GEPD does bare camx += ..., and the game never wraps vv_theta in on-foot play either (bondviewApplyVertaTheta only sin/cos's it; tank code is the sole wraper). Correction to M-122's suspect (a) mechanism: the theory that the spike feeds the crosshair via speedtheta does not hold — speedtheta comes from stick input (bondview2.c:6056, analogTurn/70 or the aim-turn easing), never a vv_theta delta; all solo-play consumers of vv_theta are sin/cos or (360−θ) rotations (raw-delta consumers at bondview2.c:1361 swirl / :9397 backstab check are MP-only). So the wrap fix is correct-vs-GEPD hygiene, but likely NOT the visible spazz.
Also resolved from static reading: suspect (b) tick ordering — our write runs BEFORE bondviewProcessInput each tick (osContStartReadData → inputComputePad, libultra.c:1094), vs GEPD writing after game code; but the displayed fixed point is X·damp+turn either way in steady state, so ordering alone can't cause spazz. Suspect (c) 2px deadband — does not apply to the GEPD path (deadband lives only in the legacy fallback). Suspect (d) centre-spring nudge — ~0.1–0.3° over its 2 ticks, negligible; kept (clears docentreupdown, which must not fight aim).
Remaining live suspect for ① (now instrumented): the game-side turn terms added on top of our overwrite every tick — turn_x = autoaimx | speedtheta*0.3, turn_y = autoaimy | −speedverta*0.1 (HONEY block bondview2.c ~6140-6170 → caclulate_gun_crosshair_position_rotation gunfire.c:4819, which does pos = pos*damp + turn). Prime suspect: auto-aim pull oscillating as lock-on jitters (displayed crosshair = our value·damp + autoaim term; matches "worst near enemies"). The gepdaim log line now also prints game=(resX,resY) (pre-overwrite residue = last tick's pos·damp+turn — the gap vs our write IS the turn term), aa=(autoaimx,autoaimy), st=/sv= (speedtheta/speedverta). A live capture while aiming at guards will confirm or kill it in one look.
Item ② (FPS drop): checked sysLogPrintf — plain buffered fprintf(stderr) (port/src/system.c:87), cheap; the ≤10fps-while-aiming session's correlation with GE_INPUTLOG is probably coincidence, but the control retest (logging OFF) still stands as first step. Frame-time lines in the old log are too sparse to use.
Repo state: fix/d194-mouse-dt-decouple @ ef49d9e6 + UNCOMMITTED: port/src/input.c (wrap removal + extended gepdaim log), docs/dev/findings.md D194 status (M-123 note). Build clean, -level_09 headless sanity run crash-free (gepdaim lines only fire while aiming — needs live mouse). d194_gepd.err still in repo root (mined; can be deleted once the new capture supersedes it).
M-123 update (user playtest of a7059c46+bd6caef5, sens 25): user verdict — "looks good... other than sensitivity we're good, 95%." ① is ACCEPTED as-is (residual arm/gun/crosshair glitching deemed playable; not worth another round). The d194_gepd2.err capture came out EMPTY (PowerShell 5.1 2> redirect on a native exe didn't capture stderr — use the Start-Process -RedirectStandardError form from M-122 if a capture is ever needed again). Remaining work = ③ sensitivity co-tune, live: ini now at GepdSens = 30 (was 25; code default also 25 — resync the default once the user lands on a number they like). Tune loop: close game → edit data/ge007.ini GepdSens (1–80) → relaunch; no rebuild needed. If 30 is too fast, bisect 27/28. Once settled: update code default + this note, then re-test ② (aim-mode FPS with logging OFF — never properly controlled yet) before considering the D194 branch merge-ready.
M-123 CLOSED (this session): sensitivity co-tuned live and merged to main as f239adf5 (fix/d194-mouse-dt-decouple, branch deleted). Final state: hipfire gain recalibrated (MouseTurnSpeed default 100→50; the old gain saturated the game's quadratic natural-turn curve at ~13 px/poll = always full 315°/s, the "too fast when I leave aim mode" complaint); MouseSensitivity master now scales aim mode too so both modes move in lockstep (D238 step-1); defaults set ~25% above the user-approved match point (AimModeSens = 38, MouseTurnSpeed = 50, master 100 = calibrated baseline). User-facing key renamed Input.GepdSens → Input.AimModeSens (community name; old key kept as deprecated alias, internal gepd* identifiers kept for GEPD provenance). D194 is now fully closed: ② (aim-mode FPS) resolved by user report — "no performance [issue] is good now" during the calibrated playtest sessions; the original ≤10fps complaint did not recur, so it's closed on user evidence (the formal logging-off control was never run; revisit only if an FPS regression appears later). Playtest logs d194_gepd*.err deleted. Findings/HANDOFF closure notes are recorded but uncommitted by user request — commit them with the next change.
M-122 (2026-09-13) — D194 GEPD-mirror aim IMPLEMENTED + user-confirmed "mechanically works"; three items left: ① crosshair/arm spazz glitch, ② aim-mode FPS drop (perf.ps1), ③ sensitivity tuning. Next session starts at ①.
The aim rewrite landed. After four failed stick-synthesis models (absolute → dial → relative-angle → velocity-mapped stick; see M-121 + commits 8a8d066b/650b7f63/7f65ed15), the session found and read GEPD's actual source (<games>\Emulators\Nintendo 64\1964_GEPD_Edition\1964\source.tar.xz → MouseInjectorPlugin/games/goldeneye.c, extracted copy at <temp>/gepd-src/) and mirrored its model exactly. User verdict: "mechanically it works... the aim mode works how we'd want." Committed as ef49d9e6 on fix/d194-mouse-dt-decouple (branch is up to date with main; PR #75/#76 already merged in).
The model (all in port/src/input.c, aimGepdCompute() ~line 1377): mouse delta moves a crosshair POSITION accumulator (s_gepdCrossX/Y, clamped ±GEPD_CROSSHAIR_LIMIT=5.16); each tick it writes crosshair_x/y_pos + gun_azimuth_angle/turning (GEPD formulas, failsafe weapon offsets 0.15/0, RATIOFACTOR=1 for our 4:3, GEPD_BASE_FOV=90) and edge-scrolls the camera only past 72% toward the screen edge ((ratio-0.72)*475*dt*(fov/90)). No look stick is emitted while aiming. On aim release we stop writing and the game's own damping eases crosshair+arm back to centre; every non-aim tick adopts crosshair_x/y_pos so re-entry starts where the game left it (GEPD does the same).
The key correction this session (cost a broken build): GEPD patches nothing in bondview2's damped crosshair update (caclulate_gun_crosshair_position_rotation, gunfire.c:4819) — its per-frame overwrite simply wins over the damping. The first implementation guarded that function off via portAimOverrideActive() → crosshair froze at centre, because the visible HUD crosshair + aim ray are drawn from crosshair_angle (gunDrawSight gunfire.c:6395; get_bullet_angle :4932) and the arm offset from field_FFC (gunUpdateAndFire ~gunfire.c:540-547, scaled by weapon stats PlayX/PlayY) — both derived only inside that function. Guard removed entirely; bondview2.c now carries only an explanatory comment. There is NO permanent game-code change — the previously-approved bondview2 exception was never needed. (findings.md §F D194 still describes the old attempts — update it this session.)
Field mapping verified: GEPD's N64 RAM offsets (crosshair 0x9F0/0x9F4, gun 0xA04/0xA08, cam 0x148/0x158, aimflag 0x124) differ from our 64-bit struct by a constant 0xBE0 = pointer-width expansion between vv_theta (0x148, matches exactly) and the crosshair block. Field names are correct; do not "fix" them toward GEPD's numeric offsets.
Remaining work, in user-specified priority order
① Crosshair/arm "spazz" glitch (TOP PRIORITY). User: crosshair "glitches or spazzes out and moves around a bunch, so does the positioning and Bond's hand/gun." Ranked suspects:
- (a)
vv_theta[0,360) wrap inaimGepdCompute— if the game holds a negativevv_theta(turning left), the first edge-scroll tick snaps it +360. sin/cos consumers see nothing, but anything doing raw deltas (e.g.speedtheta, "computed from vv_theta") gets a one-tick 360° spike → HONEY feedsturn_x = speedtheta*0.3into the crosshair (bondview2.c ~6157) → visible slam. GEPD just doescamx += ...with no wrap. Fix: delete the two wrap lines. - (b) Tick ordering vs auto-aim/view-speed turn terms. HONEY's caclulate_ input is NOT the stick — it's
autoaimx/autoaimy(target lock) orspeedtheta*0.3/-speedverta*0.1. Those get added tocrosshair_x_posevery tick on top of our overwrite; ifinputComputePadruns BEFOREbondview2ProcessInputin a tick, the derivedcrosshair_angleshown that frame includes damp-shrunk value + turn term → one-frame tugs, worst near enemies (auto-aim snap) and while edge-scrolling. Check call order; GEPD's plugin writes after game code each frame — if ours is before, consider deferring the write to a post-update hook. - (c) 2px/poll deadband stick-slip (
AIM_ABS_DEAD_PX) at low mouse speeds — whole-poll drop makes the crosshair crawl in steps; GEPD has no deadband (emulator deltas are clean). Consider subtractive deadband or dropping to 1px. - (d)
s_centreClearTicksspring-clear nudge (2-tick ±61 pitch stick on aim entry, docentreupdown) — with caclulate_ running again it feedsturn_y→ small crosshair jump at aim entry. GEPD doesn't do this; candidate for removal. - Diagnosis first: repro with
GE_INPUTLOG=1, capture the log (stderr → file, see below), look for 360° jumps incam=(...)and oscillation incross=(...)in thegepdaimlines. Repro into a level without menu navigation:./build-pc/ge007.x86_64.exe -level_09(BUNKER1) or-level_33(Dam); drive input withbuild-pc/aiminject.exe [dx dy steps holdms](RMB + relative moves, finds the "GoldenEye" window).
② Aim-mode FPS drop. User saw ≤10 FPS (DisplayFPS overlay) in the last playtest session; hip-fire/menu fine. First control: that session had GE_INPUTLOG=1 — retest with it OFF before anything else (per-tick sysLogPrintf while aiming). Then run tools_pc/perf.ps1 (ETW/xperf, needs elevated PowerShell): baseline perf.ps1 -level_33 -Seconds 30 without aiming, then a second capture while holding RMB + moving the mouse (user or aiminject), diff cswitch_thread.csv/profile_detail.csv. Caveat from its header: our exe's DWARF symbols don't resolve in WPA — hot frames show as raw module; symbolicate sample addresses with addr2line -e build-pc/ge007.x86_64.exe -f -C <pc> (image base 0x140000000). Suspects if CPU-bound: auto-aim scan while RMB held (verify it's actually NEW cost vs legacy aim, which also held RMB), viewmodel skinning, the edge-scroll camera writes. If NOT CPU-bound: render-path stall (crosshair/decals/draw calls) — instrument frame time in port/fast3d/gfx_pc.cpp.
③ Sensitivity tuning (LAST, user will co-tune live). Knob: Input.GepdSens (default 20, range 1–80) → sens = gepdSens/2920 per window-px. GEPD's own formula for reference: crosshairpos += delta/10 * (SENS/292) * accel with their basefov=60 override; ours is adapted to native 90° FOV + no /10 chunking, so the numbers are NOT directly transferable — tune by ear/eye against "normal aiming" feel (C-stick speed levels (stick-60)/10). Also tunable: GEPD_SCROLL_SPEED (475), GEPD_EDGE_THRESHOLD (0.72) if edge-scroll feel is off; per-weapon offsetpos[32] table from goldeneye.c if the arm pose looks wrong for specific weapons (we use failsafe 0.15/0).
Environment / procedure notes (this session's hard-won ones)
- Build:
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-finalfrom repo root (~5s cached). The manualbuild-pc/link.batpath also works but needs itsset PATH=C:\msys64\mingw64\bin;%PATH%line (cc1.exe dies 0xC0000135 without it) and Windows-style TMPDIR. Kill any runningge007*process before linking (exe lock). - Run for playtest: from repo root,
powershell -NoProfile -Command "$env:GE_INPUTLOG='1'; Start-Process -FilePath '.\build-pc\ge007.x86_64.exe' -WorkingDirectory (Get-Location) -RedirectStandardOutput 'd194_gepd.log' -RedirectStandardError 'd194_gepd.err'"— GE_INPUTLOG lines go to stderr (d194_gepd.err), stdout stays empty. ROM at./data/ge007.ntsc-final.z64, ini at./data/ge007.ini(user's live values: DisplayFPS=1, 1152x864). - clang strictness: game files build with gcc's leniency; clang rejects implicit decls/int-conversions.
struct player *pnotPlayer *p. Forward-declare statics before use. - GEPD mouse settings live in the emulator's
plugin/mouseinjector.ini(numeric, 10 values × 4 players; couldn't fully decode — first value 87 doesn't fit the CONFIG enum). We use GEPD defaults + our own knob instead.
Repo state at session end: fix/d194-mouse-dt-decouple @ ef49d9e6, clean tree, up to date with main. No running game process (killed before close-out). **Untracked d194_gepd.err in repo root is kept on purpose: it holds the gepdaim per-tick lines from the user's "mostly feels good but spazzes" playtest of the final build — mine first evidence for ① (look for 360° jumps in cam= and oscillation in cross=). Next session: preflight per usual, then start at ① above — suspect (a) is a two-line fix worth doing before the log-driven diagnosis.
M-121 (2026-09-13) — PR #75 (D250 getenv fix) + PR #76 (D222/D218/D249 FOV/culling/drawdistance) both MERGED, user-confirmed across resolutions; RMB aim-mode still broken, two more attempts failed, session ended on usage limit mid-investigation
PR #75 and PR #76 both merged to main this session, both user-confirmed working. D250 (getenv caching fix) confirmed clean at every resolution tested, 640x480 through 4K — the initial "still bad at 1440p+" report was a stale-build false alarm, corrected in the docs. D222 (FOV-scale cull-plane fix) + D218 (Video.DrawDistance) + D249 (Video.LodDistance) landed together, then needed two live retuning passes before the user was satisfied: (1) a real regression found and fixed live — D222's c_lodscalez widening also fed propobj.c's character/prop fog-visibility-fade cutoff uncompensated, making guards fade sooner at wide FOV; composed portDrawDistanceMultiplier() into those call sites to fix it. (2) The DrawDistanceAutoFov coupling needed doubling twice live (1x -> 2x -> 4x FovScale, clamp ceiling 2.5x -> 4.0x -> 6.0x) before the Dam tunnel's blue fog artifact and guard draw-distance were acceptable. User's own final verdict: "good for now." Repo is clean on main after both merges.
RMB aim-mode (D194 continuation): two more attempts this session, both did not land, session ended mid-plan on the weekly usage limit.
- Attempt 1 (small, safe): raised
AIM_MOVE_THRESH0.3->2.5px and loweredInput.MouseAimSpeeddefault 16->14 (matching the user's live ini, which had drifted to 50). User's verdict: "didn't actually fix anything" — confirms the core problem isn't a magnitude/gain issue, it's the shape of the response. - Real root cause found via code reading (still valid, not invalidated by what follows):
bondviewProcessInput(s8 stick_x, s8 stick_y, ...)insrc/game/bondview2.ctakes a signed 8-bit stick — the smallest representable nonzero aim-turn value is a hard jump to 10% speed (stick=61;(stick-60)/10is exactly 0 at stick<=60). No threshold/curve tuning on the port's synthesis side can produce a smooth ramp through an integer that can only be 60 or >=61. Confirmed viapd_port(<repos>\pd_port) reference: PD's own mouse-look (src/game/player.c:3669,src/game/bondeyespy.c:945) never synthesizes an N64 stick at all -- it'stheta += mdx * scale, a continuous float angle accumulator, no floor, no int8 quantization anywhere. - User also reported a second, more specific symptom mid-investigation: aim resets to center the instant mouse movement pauses (even while still holding RMB), breaking precision tasks (shoot-the-lock, watch-laser-cuts-metal-in-train puzzles). Traced
bondviewCurrentPlayerUpdateSpeedTheta/speedVertaUp/Down(bondview2.c) in detail: confirmed this is a smoothed rate-based decel model (g_CurrentPlayer->speedthetaeases toward 0 over several ticks when input stops, does NOT hard-reset) -- so a literal snap-to-center was not found in that specific mechanism. Root cause of the reset symptom specifically was not conclusively identified before moving to the rewrite. - Attempt 2 (bigger, PD-style, approved by user before starting): bypassed the s8-stick synthesis entirely for aim-mode.
port/src/input.ccomputed a continuous -1..1 turn/vert speed (inputGetContinuousAimSpeed(), new function + header decl) instead of synthesizingsx/sy;src/game/bondview2.c's aim-mode block (#ifdef PORT) read it directly, bypassing the(stick-60)/10conversion but leaving all downstreamspeedtheta/speedvertaeasing untouched. Removed the now-deadInput.AimBand/Input.MouseAimCurveknobs. Compiled clean (verified both PORT and N64#ifdefbrace balance carefully), crash-free in a headless sanity check -- but broke live: "somethings broken... can't aim at all... it locks my input completely with the mouse." Root cause of THAT breakage was not identified (no repro available headlessly; ran out of session before a careful live-debug pass). Fully reverted (git checkout -- src/game/bondview2.c port/src/input.c port/include/input.hback to thedb6eba2bmerge commit) — repo confirmed clean, PR #62's mouse work is back to its pre-session (working, if imperfectly-tuned) state. Live ini'sMouseAimSpeedalso manually restored 14->16 to match the reverted code default.
Best-guess candidates for what broke Attempt 2, NOT verified, next session should check these first: (a) thread-safety -- aimContinuousTurnSpeed/VertSpeed were plain static floats in input.c, read from bondview2.c with no synchronization, and this port has separate scheduler/game/input threads (D24/D134 territory); (b) bondviewProcessInput/MoveBond may run multiple times per rendered frame during multi-tick catch-up (per D193's earlier finding that g_ClockTimer can exceed 1), so the continuous value could have been consumed repeatedly per single mouse sample in a compounding way neither of us anticipated.
Proposed next step (discussed with the user, not yet started -- session ended before implementation): a safer, lower-risk redesign that stays entirely inside port/src/input.c, never touching bondview2.c at all. Instead of deriving the synthetic sx/sy fresh from only the current poll's raw delta, maintain a smoothed/persistent stick value (exponential filter on the output) that ramps toward a target implied by recent mouse speed and decays gradually on release, rather than snapping instantly in either direction. This softens both the floor-crossing (builds up over a few polls) and the release/reset behavior (decays instead of vanishing), while keeping the change confined to one already-well-understood port file -- easy to revert, easy to reason about, no #ifdef PORT game-logic surgery. Add a temporary debug log (GE_INPUTLOG-gated) that prints the smoothed value every poll, so it can be sanity-checked from a log BEFORE handing back to the user for a live test -- the process this session was explicitly asked for and didn't happen before Attempt 2.
Repo state at session end: clean, on fix/d194-mouse-dt-decouple (still holds PR #62's D194(b)/D238/natural-pitch work, unmerged, mergeable, behind main by the two PRs merged this session -- rebase before resuming). No uncommitted changes. No running game process.
M-120 (2026-09-13) — D250 ROOT-CAUSED AND FIXED (uncached getenv() ~50% of render-thread CPU time); PR #75 open; PR #62 aim-mode still needs work; new tools_pc/perf.ps1
D250 resolved this session — the fps drop was getenv(). Set up a real profiler (tools_pc/perf.ps1, new — wraps xperf CPU-sampling + context-switch tracing, sister script to debug.ps1) since ad-hoc timers had ruled out gfx_run/lvlRender without finding the cost. A live WPR/xperf trace on Dam with Microsoft's public symbols resolved (-Symbols) found the dominant render thread spending only 5.2% of its CPU time in the game's own code — 43% in msvcrt.dll, and getenv()/_getenv_helper_nolock alone at ~50% of the thread's total CPU time. Root cause: port/fast3d/gfx_pc.cpp's import_texture() (every texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe checks called getenv() uncached on every call, unlike the safe cached pattern used everywhere else in this codebase — tens of thousands of locked, locale-aware Windows CRT environment lookups per second on a texture-heavy scene. Fixed: cached every site in gfx_pc.cpp/gfx_opengl.cpp. Verified: the exact idle-Dam test that was pinned at 45-50fps now holds a rock-steady 60-61fps, zero dips — and the user confirmed live at 640×480 it "seems fine" now. This also retroactively explains the earlier session's MSAA/aniso/FOV/resolution/vsync A/B pass all showing "no change" — the getenv() cost was a large per-call constant swamping any real, smaller GPU-side signal at every setting tried. PR #75 pushed (fix/d250-frame-pacing-sleep-units, now titled around the getenv fix — also carries the earlier sysSleep unit-conversion fix and the GE_D250 lvlRender probe from this same investigation), CI running at session-end, not yet merged.
"Still runs badly at 1440p+" turned out to be a stale-build false alarm — user re-tested 1440p/4K on the actual getenv-fix build and confirmed it holds up fine there too. D250 is fully resolved, all resolutions tested clean, no follow-up needed. One gotcha hit setting up perf.ps1: its first version was missing debug.ps1's mingw64-bin-on-PATH fix and failed with a DLL load error from a plain elevated shell — fixed, same PATH-prepend logic now in both scripts.
PR #62 (mouse feel) status unchanged from earlier this session — hipfire/primary look confirmed good by the user; aim-mode (RMB/Shift) still too sensitive, AIM_STICK_MIN=61 hard-floor hypothesis recorded in findings.md D194, not yet fixed/reverified. Deprioritized this session in favor of the performance investigation once it became clear perf was confounding the aim-mode feel-check anyway.
M-120 EARLIER (2026-09-13) — PR #62 rebased onto main (picks up D248); user playtest: hipfire good, aim-mode still too sensitive; performance diagnosis (D250) queued next
PR #73 merged, fix/d194-mouse-dt-decouple (PR #62) rebased. #62 was 15 commits behind main, missing D248's 60fps fix among others — merged main in (two doc-only conflicts in findings.md/findings-index.csv, resolved by keeping the newer main-side text + regenerating the CSV), rebuilt clean (ninja -j1), pushed. A headless verify.sh bunker1 sanity check afterward reported NO-FRAMES (pixcount 0), but the script's own capture-directory (mktemp//tmp) plumbing looks broken in this shell environment (its run.log write itself failed with a path error) — looks like tooling, not a real regression; not chased further, worth a look next session.
User played PR #62 live. Verdict: hipfire/primary look is GOOD, aim-mode (RMB/Shift) is still too sensitive and unusable for precision aiming. Updated D194's status cell with the detail (docs/dev/findings.md). Static re-read of port/src/input.c's aimHeld branch found a plausible concrete cause the M-107 curve fix didn't address: AIM_STICK_MIN=61 is a hard floor the stick snaps to the instant |aimEdx| >= AIM_MOVE_THRESH (0.3 px/poll) — the aimCurveGamma response curve only shapes the range above that floor, so tiny mouse nudges still jump straight to 10% of max aim-turn speed with no near-zero ramp, unlike the hipfire path (fully proportional, no floor) the user says feels right. Not build/runtime-verified yet — a hypothesis from reading the code, not a confirmed fix. PR #62 stays held, unmerged — needs another aim-mode iteration + re-playtest before it can land.
User also reports the port is now performance-limited — ~45fps at 720p on a high-end PC — which makes it hard to judge feel further. This is the user-facing surface of D250 (filed M-119, tied to D248 exposing real per-frame cost that used to be invisible while the port silently ran at half rate). Same session: diagnosed partially, real bottleneck not caught yet. Ruled out: build flags (-O2 RelWithDebInfo confirmed the actual default) and idle-load — GE_D193=1 on bunker1 and streets both idle at a healthy 55-60fps in this dev box, well under budget, not reproducing the user's 45fps. So the real cost is very likely gameplay-load-dependent (AI/combat/particles active, not an empty idle scene) or resolution/GPU-fill-rate-bound in a way idle testing can't exercise — this session had no way to drive real keyboard/mouse input into the game headlessly to generate a loaded scene. Docs-only PR #74 (docs/d250-perf-diagnosis, off main) carries the write-up. Same session continued, live with the user driving the game on Dam. PR #74 merged (docs-only, uncontroversial). Confirmed live: sim stays healthy 59-67/s, render sags to 45-50/s, frame rendered in Nus (gfx_run cost) stays 6-10ms — comfortably under budget. One-at-a-time A/B via the F10 overlay ruled out MSAA, anisotropy, FOV, and resolution (1280×960 fullscreen vs 640×480 windowed — zero change) and VSync (render never exceeds ~60 even fully uncapped, and the bad numbers didn't move) — the cost is resolution/GPU-setting-independent. Found and fixed a real bug in passing, but confirmed NOT the active cause here: sync_framerate_with_timer() (port/fast3d/gfx_sdl2.cpp) passes a 100ns-unit value straight into sysSleep() (expects microseconds) — a genuine 10x oversleep, but only on the target_fps-gated path, which was inactive in the user's FpsCap=0/VSync=0 test config. Fixed anyway (correctness). Added GE_D250 (src/boss.c, wraps lvlRender()) to check DL-building cost separately from gfx_run — also cheap (~0.5-1.5ms), also not it. So: two most likely CPU call sites both ruled out, ~10-15ms/frame still unaccounted for. Leading remaining suspect: the emulated N64 thread-handoff/scheduler round-trip (gesched.c/sched.c) — real OS thread wake/context-switch latency standing in for what was free hardware handoff on N64. Next: needs a real profiler (Visual Studio's sampling profiler, or WPR) attached to ge007.x86_64.exe during a live bad-fps moment — ad-hoc printf-timers have now ruled out the obvious spots without finding the cost; more of the same probe-and-guess approach has diminishing returns. PR #75 (fix/d250-frame-pacing-sleep-units, off main) carries the sysSleep fix + GE_D250 probe + full write-up.
M-119 (2026-09-12) — docs hygiene pass (findings-index regen + stale-status fixes); D249/D250 filed
Docs-only session, no code changes. Did the roadmap's own overdue
housekeeping item (regenerate findings-index.csv + fix stale status cells)
and logged two new user-reported issues.
docs/dev/findings-index.csvregenerated viatools_pc/gen_findings_index.py. Only one row actually changed (D194) — everything else the roadmap flagged as potentially stale (D219, D228, D229, D233, D245-D248) turned out to already be current onmain; a naive python cell-split I used mid-session falsely flagged several of these as stale before I re-checked with the Read tool — don't trust a naive" | ".split()on this file, several rows have pipe characters inside code spans/backticks that break it.- D194's status cell was genuinely stale ("root cause and fix direction known, not implemented") — corrected to reflect that part (b) (dt-decouple) has been merged since M-107, and part (a) (aim curve) + D238 (master sensitivity) + the SOLITAIRE natural-pitch scheme switch are all implemented and build-verified on PR #62, held only on the required human playtest.
docs/dev/GRAPHICS-BACKLOG.md's D233/D234 rows were stale ("Observed, not investigated") despite both being FIXED + visually verified since M-107/M-108 perfindings.md/the index — corrected. Same doc-lag class the roadmap has flagged repeatedly (D77/D217/D227 previously).- Two new findings filed (user QA report): D249 — LOD/culling breaks
down on large open levels (Streets, Egyptian) at default FOV, far
geometry specifically; distinct from D222 (high-FOV near-edge culling) and
D218 (fog tint boundary); also a QoL ask to make any LOD improvement
togglable so N64-accurate LOD stays selectable. D250 — FPS is
inconsistent, dropping to 20-40fps intermittently on many levels since the
port started actually running at 60fps; very likely a direct consequence
of D248 (M-118) exposing real per-frame cost that was invisible while
the port silently ran at half rate — D248's own write-up already flagged
this exact risk as untested. Neither investigated yet; both filed in
findings.md§F (regenerate the index whenever these are touched again) and D249 additionally inGRAPHICS-BACKLOG.md.docs/dev/notes/ROADMAP-1.0.mdanddocs/dev/UNLOCKED-FPS-PLAN.mdgot short addenda pointing at both — the FPS plan's "default = ~30fps" premise is now stale post-D248 and needs a Phase-0 re-measurement before it resumes, likely gated on D250.
Not done this session: any actual investigation of D249/D250 (this was a
docs/triage-only pass, no build or runtime access exercised); the
bunker1 golden re-baseline / full verify.sh sweep still owed since D217;
PR #62's human A/B playtest (still the top blocker on Track C).
M-118 (2026-09-12) — D248 FIXED: the port has been running at 30fps instead of 60fps game-wide; frame-rate bar B now unblocked
User asked to verify the game holds a stable 60fps post-timing-keystone (bar B). It did not — found and fixed a real, previously-undiscovered bug that had been halving presentation/tick rate to 30fps since the port's inception. Root cause: src/sched.c osScAddClient() stashes a per-client "every retrace vs. every other retrace" flag by writing into the next array slot's .next field (c[1].next = next) instead of a real struct member — gfxClient gets NULL (60Hz), the audio client gets a nonzero sentinel (30Hz, its correct rate). __scHandleRetrace reads that flag back via a hardcoded *((s32*)client + 2) byte offset that only lands on the right memory when sizeof(OSScClient) is 8 bytes (32-bit pointers, true on N64). On this 64-bit port sizeof(OSScClient) is 16 bytes, so the same offset instead reads into the middle of client->msgQ (always non-NULL) — the gfx client's flag reads as permanently set, and it silently falls onto the audio client's 30Hz path. Same pointer-width-growth bug class as D122/D126/D132/D209, just in the scheduler. Fix: #ifdef PORT reads client[1].next == 0 (indexes the struct, scales correctly with pointer width) instead of the hardcoded offset; N64 line kept verbatim under #else. Verified live: Video.DisplayFPS=1 overlay read a rock-stable "30 FPS" pre-fix and "60 FPS" post-fix on -level_09 (BUNKER1), same build/scene, GE_PCDUMP screenshots both ways. Rebuilt clean; -level_09/-level_20 both ran crash-free post-fix. §F D248, porting-notes.md §A. Working tree has the one-line-plus-comment src/sched.c change, docs/dev/findings.md D248 row, porting-notes.md D248 entry, findings-index.csv regenerated — uncommitted, review before landing.
Environment note (recorded once already at M-108, re-hit this session): ninja (parallel, default build-pc.sh invocation) fails every object with Cannot create temporary file in C:\Windows\: Permission denied on this box even with TMP/TEMP correctly set. Workaround: cd build-pc && ninja -j1 (single-threaded) builds clean every time. Use -j1 here until someone roots out the real cause.
Not done this session: verify.sh sweep across all 21 levels (only -level_09/-level_20 spot-checked); a live human playtest for feel now that the game actually renders/ticks twice as often per second (should feel smoother/more responsive, not different in speed — deltaFrames-based catch-up should have kept real-time game speed correct even while halved, but this wants eyes-on confirmation); re-measuring docs/dev/UNLOCKED-FPS-PLAN.md Phase 0 baseline now that the "default preset" is actually 60fps, before scoping whether a 120fps preset targets 1.0 or post-1.0 (this was explicitly asked as a scope call — still open, see below).
C track (mouse/input, bar #6) not started this session — D248 took the full session once it turned into a real bug hunt instead of a quick measurement. Still queued next: D194 full pass (aim curve + SOLITAIRE controldef + D238 sensitivity, on held PR #62) → user A/B playtest → merge; then D239 click-to-lock parity; D192 grid pointer; playtest debt (D223 wheel-cycle, focus-loss mute).
M-117 (2026-09-12) — D195 + D228 FIXED (both user-verified live); D245 REOPENED; D247 closed NOT A BUG; D246 narrowed
Worked the M-116 punch list (D245 → D247/D246 → D195 → D228), committing each as it landed. Two real, previously-undiscovered bugs found and fixed this session, both confirmed by the user in live play:
- D195 FIXED —
G_TEXTURE_GENenvmap UV generation was missing entirely. User reported gold/silver guns rendering solid black, which redirected the stale D195 (Control glass) investigation onto the real, much bigger bug: an earlier session (D72.1) had removed the PD-inherited envmap block wholesale to fix the Rareware logo, instead of gating it on theG_TEXTURE_GENbit that actually distinguishes the logo (doesn't set it) from every shiny/reflective material in the game (does). Restored the envmap UV generation from the reference PD port, gated onG_TEXTURE_GEN. User-verified: console glass and gold/silver guns are reflective again, not flat black. The residual "not translucent" complaint on the glass is confirmed NOT a bug — user confirmed it was never see-through on the original N64 either (reflective by design). Full write-up + porting-notes lesson: findings.md D195. - D228 FIXED — IA16 palette intensity/alpha byte order was swapped.
GE_TEXDUMP+a throwaway correlation probe on Facility ruled out the original texpool/RC2-clip hypothesis (decoded size was always correct) and foundpalette_to_rgba32'sG_TT_IA16branch reading intensity/alpha backwards relative to the already-correct direct-IA16 decoder two functions above it in the same file. Fixed the swap. User-verified: AK47 and other metallic guns render correctly, no more white patches. - D247 (black bars top/bottom) CLOSED as NOT A BUG. Traced to
bondviewGetCurrentPlayerViewportHeight()'s unmodifiedVIEWPORT_HEIGHT_DEFAULT(220/240 NTSC scanlines) — genuine, faithful N64 TV-safe-area letterboxing, confirmed identical on a fresh EEPROM (rules out a stale Screen=Wide/Cinema setting). No code change. - D246 (left/right edge strips) narrowed, still OPEN.
GE_PCDUMPcaptures show clean edge columns in the native render, ruling out an RDP/game-DL bug — the defect must be introduced in the SDL window presentation path, which headless capture doesn't exercise. Needs the user's window size /Video.MSAAsetting to chase further. - D245 (water seam) REOPENED. The
allowShift=FALSE→TRUEfix (mechanistically sound-looking, confirmed via aGE_D227Vstatic trace) did not fix the live symptom — user reports the water still flips between two patterns and still tracks view angle, unlike the sky (same underlying mechanism, no longer broken there). The static single-frame/single-angle trace this session used was evidently insufficient evidence; needs a fresh investigation with a live or multi-frame-rotating capture, not a repeat of the same approach. Don't re-apply the same reasoning without new evidence.
Process note, worth repeating for future sessions: verify.sh and any taskkill //IM ge007.x86_64.exe kill whatever game instance is currently running — including one the user has open themselves for manual testing. Hit this mid-session: ran verify.sh while the user was actively testing a fix, killing their window without warning. Always ask before running verify.sh/launching/killing the exe if there's any chance the user has it open. When the user is actively testing, do documentation-only work and wait for their report rather than re-launching to check yourself.
Not done this session: a full verify.sh sweep after D228 (only a headless single-level check + user's own live playtest); D246's live-window follow-up.
M-116 (2026-09-12) — D229 (green/pulsating IsWater water) ROOT-CAUSED AND FIXED (port-layer); next: D195
D229 closed out this session. The earlier "one texunit resolves blue and the other green" framing was wrong on mechanism, right on symptom. The water image (skywaterimages[2] = texnum 1509 IMAGE_WATER_BLUE) is ZLIB+CI8: the texture pool holds raw 8-bit palette indices (dumped pool block: 1400 bytes, every byte ≤ 23 — the 24-colour blue palette's index range; full mipmap chain + row padding). texSelect loads it with gDPSetTextureImage(CI, 16b) + gDPLoadBlock (the 16b is only fast3d's 4KB-per-block convention: 700 × 2 = 1400 bytes of 8-bit data). On N64 GE's custom RSP ucode expands indices→RGBA16 in TMEM at load time, so sub_GAME_7F09343C can re-declare TMEM slot 0 as RGBA/16b and sample blue. fast3d had no expansion: import_texture_rgba16 read index byte-pairs as 16-bit texels — g = 2·idx mod 32 dominates, r/b ≤ 2 → green mottle; the "pulsating" was just the pre-existing sinf()-driven PRIM_LOD_FRAC cross-fade between the two mis-decoded tiles. (The N64 side is provably expanded: no shade of a G-dominant texel can multiply into B>G output.) Fix (port-layer only, port/fast3d/gfx_pc.cpp): texture_to_load.fmt was previously dropped — now recorded into LoadedTexture.src_fmt by gfx_dp_load_block/gfx_dp_load_tile; in import_texture, an RGBA/16b tile whose slot's last load came from a CI source routes through import_texture_ci8 (palette expansion via rdp.palette) — the port equivalent of the ucode's expand-at-load. Fires only on the genuine format disagreement; real RGBA16 textures untouched. Verified: Frigate -level_26 live run, water renders blue, user confirms "looks better". A GE_D229-gated confirm log (one-shot) remains in import_texture; the ad-hoc SetTexImage/LoadBlock/LoadTile probes were removed. Still owed before the row is fully closed: standard verification ritual — verify.sh sweep + by-eye on Surface 2 (the other IsWater level). Docs updated: findings.md D229 row + §H resolution, index csv regenerated (D229 → FIXED), ROADMAP Task 1 item 1(a) struck through.
New user bug reports filed same session (documented only, not investigated): D245 IsWater water shows two competing patterns + a view-tracking seam — "looks almost exactly like skybox"; strong lead is the documented allowShift=FALSE s16-tc-overflow tolerance in sky.c skyPortBeginFan (D227's mechanism, made visible by the D229 fix); D246 tiny raw-pixel strips at the left/right screen edges ("overscan") — suspect present/scale blit rounding; D247 black bars top/bottom — possibly intended 4:3 letterbox pending the parked WIDESCREEN-FOV-PLAN, needs the user's window aspect to tell. All three are in findings.md §F + index.
Next (ROADMAP-1.0 Task 1): item 1(b) D195 Control transparency — uninvestigated, needs a capture first. Then 1(c) D228 white NPC-weapon patches. Unchanged elsewhere: PR #56 still blocked on filing the fast-alt-tab audio-restore-race Dxx (index topped at D244); PR #62 held for the combined D194(a)+SOLITAIRE+D238 pass; bunker1 golden stale until re-baselined.
M-113 (2026-09-12) — D219 (purple explosions) FIXED: genuine byte-order bug in the runtime texture decompressor (next session starts here)
D219 root-caused and fixed this session. A dispatched subagent (M-112) hit a dead end — its diagnostic probes never fired because they filtered on the GBI tile's declared width (16) instead of the bitstream's real decoded width (14), and it also hit an environment issue getting stdout from the game exe. Picked it back up interactively: fixed both (the real width is 14×14; the "no stdout" issue was timeout stripping the child's env in that shell — a plain background & + taskkill //IM gets it fine here) and traced the actual bug to src/game/image.c's runtime texture decompressor (texReadUncompressed, texChannelsToPixels, texBuildLookup, texInflateLookup, texInflateLookupFromBuffer — all ground-truth decomp, never modified before now). These reconstruct each wide (>1 byte/pixel) pixel as a shifted-OR integer and commit it with a native machine store (dst16[x] = value) — correct on N64 (native store = big-endian, what port/fast3d/gfx_pc.cpp import_texture_rgba16 expects), silently byte-reversed on this little-endian host. Only IMAGE_FIRE_0..14 hit it because they're one of the only assets that are both genuinely 16-bit-per-pixel (RGBA16) and runtime-decompressed (static C-array textures are already normalized by D71's gfx_tex_normalize_source; every other runtime-decoded format in the game is single-byte/no-endian). Fix: PORT_PIXEL16/PORT_PIXEL32 macros (__builtin_bswap16/32 under #ifdef PORT, identity under #else) wrapped around every wide-pixel store in those five functions — narrow, mechanical, N64 build byte-for-byte unaffected. Verified: GE_TEXDUMP PPM captures of all 15 fire textures flipped from B-dominant (pre-fix, matches M-111's finding) to R-dominant/warm (post-fix); a 10x-upscaled render and a 6-frame animation strip show a clean fire-sprite look (bright core → orange → red/ash), no purple/blue. A 12-texture montage of unrelated Dam world textures confirmed no regression. -level_09/-level_33 both boot and render crash-free post-fix. Full write-up: docs/dev/findings.md D219 row, "M-113" paragraph; new porting-notes.md entry under §C (native-store byte-order bugs in runtime-decoded multi-byte pixel formats). Not yet done, next session: a full verify.sh sweep (21 levels) and a live human playtest of a real (non-scripted) firefight/explosion to close the row for good — everything here is scripted-repro + texdump evidence, strong but not a human eyeball on the actual game. Working tree left uncommitted (src/game/image.c, docs/dev/findings.md, docs/porting-notes.md) for review before landing.
M-108 (2026-09-12) — D194(b) played, held (not merged); D160 finally live-traced, real root cause found and split to D243; D240/D241/D242 filed
Environment gotcha, hit hard this session, fix it first: this box's Bash tool is Git Bash, not true MSYS2 — MSYSTEM=MINGW64 is set but /mingw64 doesn't resolve. Every build/run command this session needed export PATH="/c/msys64/mingw64/bin:/c/msys64/usr/bin:$PATH" prepended first. Also: ninja -j<N> (parallel) failed every time with Cannot create temporary file in C:\Windows\: Permission denied from gcc, even though TMP/TEMP were correctly set and writable — looked like an env-propagation race under -j. ninja -j1 (single-threaded) built clean every time, no code changes needed. Use -j1 for this project on this box until someone roots out the real cause. Also: the exe resolves save/config paths as data\ relative to CWD (port/src/system.c:212,244) — launch it with CWD at the repo root (or wherever data/ is a direct child), never from inside data/ itself, or saves/config silently fail into a nonexistent data\data\ path with no crash, just silent write errors in the log.
D194(b) played (PR #62, fix/d194-mouse-dt-decouple): real improvement, not enough — PR stays unmerged. User's verdict: "better by default but still feels bad. vertical input is different than horizontal, right mouse aiming is still way too sensitive." Both residual complaints match already-catalogued, not-yet-implemented parts of D194 exactly — nothing new to investigate: RMB over-sensitivity = D194(a) (the aim-mode response curve, separate [61,80] clamp system, unimplemented); vertical/horizontal asymmetry = the CONTROLLER_CONFIG_HONEY vs SOLITAIRE/GOODNIGHT controldef finding from M-87 (hipfire pitch is digital-only under HONEY, forced by the port; SOLITAIRE frees the whole analog stick for continuous pitch+yaw, unmodified GE control scheme). User's explicit call: hold #62, do D194(a) + the controldef switch + D238 together as one pass, then re-playtest before merging anything. PR #62's CI is green/CLEAN and mergeable whenever that full pass lands and feels right — it doesn't need rework itself, it's just incomplete on its own.
D148/D160/D173 cutscene investigation: finally got a live trace this session (GE_D160=1, Dam -level_33, two debug-loads + one fresh-save normal playthrough) — real progress, not just re-confirmed dead ends. Big correction: D160's old premise ("cutscene never plays, cuts straight to mission complete") is REFUTED on the current build — the trace shows the AI trigger firing and all 3 scripted camera_switch calls executing every time. The cutscene runs; it renders corrupted. New finding D243 filed with the real mechanism: a D146 fast3d guard burst (freed/uninitialized-memory byte pattern 0xfafbfbfcfcfbfdfc, same 4 addresses repeating) fires right in the camera-cut window — a stale display-list-memory read, likely a render-pipeline race (two debug-load runs showed the AI-list's camera_switch calls landing in a different order relative to bossReturnTitleStage between runs, and the corruption appeared in one run but not the other — nondeterministic, i.e. a real race, not a fixed logic bug). User-confirmed the camera-glitch reproduces on a real fresh-save playthrough too, not just the debug-load harness, so it's a genuine bug, not a -level_N testing artifact. A second symptom seen only on the debug-loaded runs (no report screen, straight to file-select, save not landing) did not reproduce on the fresh-save run — that one is most likely a -level_N debug-boot artifact (same class as D190) and was deliberately NOT filed as its own bug; don't re-investigate it as real without a fresh-save repro. Next step for D243: an env-gated probe on whatever frees/recycles the segment or double-buffer slot backing the outgoing camera shot's geometry during the ai_17 window, cross-referenced against the 4 repeating fault addresses (session logs data/playtest_d160.log + _run2.log, not committed — regenerate by repeating this session's steps if needed).
D240/D241/D242 filed (subagent, docs-only, docs/dev/findings.md): D240 gunshot-SFX-timing (LOW-MED confidence, could be a stale pre-D209 report or a real residual in the same timing-keystone family); D241 PP7/AK47-silent-not-Klobb (HIGH confidence this is D207 reproduced — fold into D207 rather than tracking separately, the repro D207's own M-77b session couldn't get); D242 death-sequence-needs-LMB-click (HIGH confidence NOT a port bug — the 3-angle auto-death-camera and its no-input auto-transition exist unmodified in the decomp; LMB is GE's own original Z_TRIG skip button; still needs a playtest confirming the automatic no-click path actually completes promptly on PC).
D233 visually confirmed fixed this session — -level_26 (Frigate; note the correct level id, LEVELID_FRIGATE=26 per bondconstants.h, not the -level_02 guess in the old finding text) doorway/corridor geometry no longer flickers/vanishes near room boundaries. Closed in findings.md (both the detailed row and the older duplicate stub row now point to FIXED+VERIFIED).
Still open from M-107, not reached this session: D235 wrong-textures-on-death, D230 music quality, D236/D195/D229 visual backlog items. See M-107 below for the full list — it's still accurate for everything this session didn't touch (minus D233/D234/D219/D194/D160, all covered above).
Session wrap (end state): docs-only PR #64 open (docs/m108-playtest-findings, off main, CI running at session end — check status before merging) carries all the findings.md updates above (D160 correction, D243 new, D233/D234 verified-fixed, D219 reopened). PR #62 (D194b) still open and unmerged, untouched this session beyond the playtest. No src//port/ changes this session — investigation and docs only. Game process closed; repo left on main, clean except one pre-existing untracked .github/workflows/selfhosted.yml (not this session's — leave alone). Next session, in priority order: (1) merge #64 once CI is green (docs-only, should be uncontroversial), (2) decide on #62 per the user's own call already recorded above, (3) pick up D219's live-repro capture (now the top-priority open item — game-wide per the user's impression, previously believed fixed) or D243's stale-DL-memory race, whichever the user wants to tackle live next.
M-107 (2026-09-12) — D233 culling fix + cutscene investigation MERGED; D194(b) mouse fix OPEN, held for a human playtest session (next session starts here)
Three parallel tracks dispatched this session (portal/frustum-cull, cutscenes,
D194 mouse feel) all landed. Two are merged to main; D194(b) deliberately
NOT merged — it changes live mouse-feel code and needs your hands-on
confirmation first, per your own instruction this session. Next session's
job is a live playtest pass — here's the ordered checklist.
Playtest checklist, priority order
-
D194(b) — mouse-look dt-decoupling (PR #62, branch
fix/d194-mouse-dt-decouple, still OPEN). Build that branch, play with defaultInput.MouseDtDecouple=1and see if "sensitivity correlates with movement/frame rate" feels better. Then setInput.MouseDtDecouple=0inge007.iniand A/B back to the old behaviour to confirm the escape hatch still reproduces the old feel (proves the knob actually does something, not just placebo).GE_INPUTLOG=1prints alookdt scale=... raw=(...)trace line if you want to see the correction factor live. If it feels better: merge #62. Either way, D194 part (a) (the aim-mode response curve) and theCONTROLLER_CONFIG_SOLITAREcontroldef switch are still unimplemented — recommend doing those together with D238 (mouse sensitivity rework, filed this session) as one design pass once (b) is felt. -
D233 — indoor geometry vanishing near doors (PR #63, MERGED to main,
port/fast3d/gfx_pc.cpp). Fix is build-verified + crash-free but never visually confirmed (the dispatched agent's sandbox had no ROM/display). Play any doorway-heavy interior (Frigate-level_02matches the original bug screenshots' ship-corridor geometry) and walk up close to a room boundary/wall — confirm the flicker/vanish is gone.GE_PCDUMP=<range>before/after if you want a frame capture instead of eyeballing live. -
D219 — purple explosions (Dam PPK firefight). Believed already fixed by D172 (M-90 runtime re-check proved coverage) — this is just a by-eye confirm-and-close, cheapest item on this list. Blood/explosions should be dark red/orange, not magenta/cyan.
-
D234 — Dam guard towers invisible except the first. NOT root-caused (this session's investigation ruled out matrix-stack depth, D217-family cache-keying, and D233's own w<0 mechanism — see
findings.mdD234). Needs a live look at Dam (-level_10) from a distance along the dam structure — confirm the symptom still reproduces post-D233, then aGE_PCDUMPcapture is the next investigation's starting point. -
D235 — wrong textures after dying/restarting a level. Clarified this session: trigger is death/restart, NOT Facility-specific, seen on other levels too. Needs from you: does it repro on a fresh level boot (rules out reload-state) or only after death/restart (confirms it)? Which surfaces? A screenshot would help the next investigation a lot.
-
D148/D160/D173 — cutscenes (PR #61, MERGED, investigation-only, no fix). Two hypotheses ruled out this session (propDef-stride for D160; two struct-aliasing candidates for D173) — root causes still unknown for both. If you have time:
GE_D160=1on Dam, run to the exit trigger, watch for the rappel cutscene; and on Cradle (-level_41, NOT_51) watch the Trevelyan-death sequence for the float/spin (D173) reported there. Long pole for 1.0 bar #4 — a live capture here is worth more than another static-analysis pass. -
D230 — music quality (bass instrument sounds corrupted). Repro is solid already (Control elevator + regular track, Cavern elevator, Runway ~5-7s in) — no new playtest needed unless you notice it on a track not already listed. Next step is code-side (isolate the shared instrument/ patch id), not a playtest item.
-
D236 (Surface 1 billboard trees), D195 (Control transparency), D229 (Frigate/Surface2 green water) — all still open, all need a screenshot/ capture whenever you're passing through those levels anyway. Not worth a dedicated pass on their own.
Do NOT do a full 21-mission re-playthrough yet — that's the acceptance-bar
confirmation step, sequenced after the above land (see ROADMAP-1.0.md
M-10x+3). Doing it now would just re-surface everything on this list.
What NOT to re-investigate (already settled this session)
- D160's propDef/
sizepropdef-stride hypothesis is dead —CutsceneRecordhas zero pointer fields, the D122/D126/D132/D209 struct-growth bug class cannot apply here structurally. Don't re-propose it. - D173's two strongest D209-shaped candidates (
PadRecord.stanzeroing, the anim-root-motion Y chain) are both cleared —stanis ground-truth-zero on N64 too, and the Y chain was already exhaustively swept during D193/D209 (M-80/M-81). The bug is somewhere else. - The D209-style "64-bit byte-alias" bug class does not turn out to be a shared root cause across D148/D160/D173/D233/D234 — each needs its own live capture, not one more aliasing sweep.
M-106 (2026-09-12) — user QA bug report filed: D233–D236 (graphics), D237–D239 (QoL). Nothing implemented yet.
User submitted a batch of bug reports + QoL asks; all are filed, not worked:
- Graphics (→
GRAPHICS-BACKLOG.md, new rows): D233 indoor geometry invisible near doors (all levels, world objects still draw); D234 Dam guard towers invisible except the first; D235 Facility wrong textures "now" (regression — bisect vs pre-D217-fix build first); D236 Surface 1 billboard trees render as a wall of tree texture (D233 has screenshots committed atdocs/img/bugs/20260912-cullingbug-{bugged,notbugged}.png; none yet for D234–D236 — ask for them). - QoL (→
QOL-INVENTORY.mdnew rows + §F index): D237 F10 overlay wheel-scroll + more settings + category grouping; D238 mouse sensitivity rework (separate RMB aim, one linked master default, up/down ≈ left/right, GEPD Mouse Injector reference); D239 click-to-lock/always-grab — click-to-lock is already the default (mouseCaptureMode=1), user recommends dropping always-grab + removingInput.MouseCaptureMode(re-confirms the M-82 removal item indocs/BACKLOG.md, closes D192 by deletion). - All seven labels are also in the
findings.md§F index. Checkboxes live indocs/BACKLOG.md"M-106 user QA report".
M-105 (2026-09-11, continued 2026-09-12) — D223 fixed (PR #54), docs-hygiene PR #55 (+D190 closed), QoL batch PR #56, D232 no-hit-flash WIP; D230/D231 filed.
- D223 directional mouse-wheel weapon cycling: FIXED, PR #54 (branch
fix/d223-wheel-cycle). Wheel-up keeps the A-pulse forward cycle; wheel-down synthesizes the N64 "hold A, tap Z" backward combo (A-only poll, then A+Z edge →weaponBackOffset,bondview2.c:5337). Port-only, local build green (241/241) + CI green both platforms. Owes a human play-test (up = next, down = previous, no accidental shots) before merge. - Docs-hygiene PR #55 (branch
docs/stale-status-claims): the four stale claims the M-104 roadmap refresh flagged — README music (×2) + sky lines, GRAPHICS-BACKLOG D77 row, findings D217 status cell — all scrubbed; D230 filed for the in-level music quality residual (wrong instruments, elevator track; occasional garbling; suspect seq/instrument-bank decode, not the proven mixer/trigger layers). Also corrected D196's stale OPEN status (already fixed/merged6e758f1d). - D190 closed as not-a-bug (committed onto PR #55): level 45 (BASEMENT)
is MP-only — its solo setup name
UsetupimpZwas never shipped in the ROM (onlyUmp_setupimpZ), so a solo-level_45debug boot requests a nonexistent file and walks stale pool memory. Reproduced on Windows (0xc0000005inmodelLoadviadomakedefaultobj) — M-48's "Linux-only" framing was wrong; N64 is equally broken for this boot. No fix possible in the port layer (the walks need terminators no zeroed buffer provides). Bar #2 now has no known crash on either platform. - Tier-3 QoL batch: PR #56 (branch
feat/qol-mute-screenshot, off main; D231 filed): (1)Audio.MuteOnFocusLossdefault on — mixed blocks dropped while the window is unfocused (audioHandleFocus()from the SDL focus events invideo.c; gate in theosAiSetNextBufferqueue path,audio.c). Pause-on-focus-loss deliberately deferred (D204-family AI feedback-loop / scheduler interaction). (2) Screenshot hotkey productized:Video. ScreenshotKey(default F12, D214-style scancode name, parsed lazily, unknown → F12 + warning),Video.ScreenshotDir(defaultscreenshots/), timestampedscreenshot_YYYYMMDD_HHMMSS.ppmwith_Ncollision suffix; key check now scancode-based. Build green; smoke run confirmed the ini migration adds all three keys to an existingdata/ge007.ini. Owes human playtest (alt-tab mute/unmute, screenshot lands inscreenshots/, one custom key+dir). User playtest result (2026-09-12): mute-on-focus-loss works, but a FAST alt-tab-away-and-back sometimes fails to restore audio (works reliably if you alt-tab slower / pause a beat before returning) — a focus-event race inaudioHandleFocus()/video.c's SDL focus handling, not yet root-caused. User's call: park this for now, not a merge blocker — file as a new low-priorityDxxbefore merge, don't fix this session. Screenshot-key part still being tested separately. Note: PR #54/#55/#56 all touchfindings.md+ the CSV — expect small table/CSV conflicts on merge; resolve by re-applying the row and regenerating withtools_pc/gen_findings_index.py. - D232 no-hit-flash: IMPLEMENTED, verification in progress (branch
feat/qol-no-hit-flashoff main, commit3315de09, unpushed). Design:Game.NoHitFlashint config (declared + registered inport/src/video.cnext to the other route-(b) vars) + a route-(b) gate incurrentPlayerSetFadeColour(src/game/bondview2.c~4236): when the flag is set, zero any non-black RGB. Rationale (verified by exhaustive caller grep): EVERY fade routes through this one function, and the only non-black callers are the two damage paths — the per-frameg_DamageTypesenvelope inbondviewPlayerTickDamageAndHealth(all 8 rows are WHITE 0xFF,0xFF, 0xFF with an alpha envelope scaled by remaining health) and the red 150,0,0 death overlay (bondview2.c:9121) — every other caller passes (0,0,0). So zeroing RGB kills exactly the hit flash; blackouts, the colour screen and the death fade are untouched. Default 0 = original. Verification state (frame A/B viaGE_PCDUMP=1-900 -level_09, BUNKER1, 721 frames each): (a) determinism CONFIRMED — two default runs are near-identical (max Δ 0.41 mean-luminance units, most <0.01), so A/B methodology is valid; (b) default vs gate-on: the gate-on run is uniformly ~24 luminance units BRIGHTER at frames 28–45 (no colour cast: A=(57,53, 52) vs B=(81,78,77)), reconverging by ~frame 460, plus a tiny +0.15 blip in the default run at frames 473–494 (direction matches a small flash). Anomaly: white→black zeroing should make gate-on DARKER, not brighter; the simple overlay model out=S(1-a)+rgb·a is refuted (fitting requires S>255). So eitherport/fast3d's implementation of G_CC_PRIMITIVE + G_RM_CLD_SURF for fill rectangles behaves differently than assumed, or a non-black fade interacts with something else. Next steps: (1) add aGE_NOHITFLASH_TRACEenv-gated log inside the gate printing r/g/b/frac of every gated call (an edit attempt landed on the wrong branch and was NOT applied — re-apply on this branch; pattern: bondview2.c already uses getenv, e.g. GE_D160 at line ~791; sysLogPrintf isport/include/system.h, LogLevel enum inplatform.h), rebuild, run with the flag on and enumerate which non-black fades actually fire in a BUNKER1 boot; (2) read howport/fast3d/composites G_CC_PRIMITIVE + G_RM_CLD_SURF. Fallback if the anomaly resists: code-inspection stands (call sites enumerated) and the human play-test with the toggle flipped is the merge gate anyway. - Test binaries baked for the user (in gitignored
build-pc/):ge007-test-d223-wheel.exe= PR #54,ge007-test-qol-out.exe= PR #56. The plainge007.x86_64.exeon disk is a PR #56 build while the working tree sits onfeat/qol-no-hit-flash— rebuild before any D232 frame run. Untracked junk at repo root to clean:ppm_A/,ppm_B/(frame dumps;ppm/itself is gitignored),cmp_000030_{A,B}.png,cmp_000477_{A,B}.png.data/ge007.ini:NoHitFlash = 0(restored after testing); it also carries the three PR #56 keys from the smoke run — unknown to pre-#56 binaries but harmlessly ignored. - Next for the next session: (1) human play-test gates: PR #54 (wheel up
= next / down = previous, no accidental shots; inert while RMB aim held —
pre-existing), PR #56 (alt-tab mutes/unmutes, F12 → timestamped PPM in
screenshots/, custom key+dir via ini); (2) D230 repro details from the user (which instruments wrong on the elevator track; deterministic vs intermittent) — decode bug vs live voice-stealing; (3) finish D232 verification above, then file the finding + regenerate CSV + push PR; (4) merge order once tests pass: #54 → #55 → #56 (all three touchfindings.md/CSV — trivial conflicts); (5) remaining Tier-3: fog option, HUD scale, rebinding UI, FXAA toggle, resolution/integer-scale, 120fps preset, gamepad rumble, per-pad tuning. - Session close-out (2026-09-12): PR/branch state after full playtest pass.
feat/qol-no-hit-flashPR #58 opened, CI green — D232 shipped with a SECOND user-caught bug fixed: the gate zeroed RGB but notfrac(the fade's alpha), so the white flash became a same-opacity BLACK flash instead of disappearing; fixed by zeroingfractoo. User-verified twice (color gone, then flash gone entirely).docs/issue-6-dropin-rom-planPR #57 opened, CI green — local-Qwen research + writeup (repo-mapper then mechanical-coder) resolving 3 of the drop-in-ROM release plan's open questions; had to be rebased offmainafter a first push accidentally carried the D232 branch's history (branched from a commit instead ofmain— check this before pushing a delegate's doc branch next time). PR #54 (D223) fixed + user-verified, CI green. PR #56 (D231) partially verified: screenshot hotkey confirmed working, mute-on-focus-loss works but has a known fast-alt-tab-doesn't-always-restore-audio edge case — user's call: park it, not a blocker, file a low-pri Dxx before merge, don't fix this session. PR #55 unchanged, still green. All 5 open PRs (#54-#58) are CI-green and ready for merge pending the user's own final review. - D223 follow-up bug FOUND + FIXED via live user playtest (2026-09-12, this
session). User play-tested
ge007-test-d223-wheel.exe(PR #54's original build) and found the backward cycle unreliable: PP7->AK47->PP7->wheel-down sometimes correctly reached melee, sometimes landed back on AK47; separately AK47->wheel-down sometimes skipped straight past PP7 to melee. Root-caused with an instrumented trace build (tempgit worktreeat../gh-fullhist-d223,GE_D223_TRACEenv-gated logs inport/src/input.c+bondview2.c+gun.c, all reverted before landing): the wheel-down pulse staggered the synthetic "hold A, tap Z" combo across 2 polls (A alone, then A+Z one poll later). Whenever those land in 2 SEPARATE game ticks (a D117-class poll/tick-rate race), the "A alone" tick is itself a fresh A edge with no Z held ->bondview2.c'sweaponForwardOffsetformula reads that as a genuine cycle-FORWARD request and fires a REAL weapon change before the correcting backward tick runs. Trace showed literally every wheel-down after the first one in a session mis-firing forward. Fix (port/src/ input.conly): present A+Z together from the first poll instead of staggering —weaponForwardOffsetrequires Z NOT held, so simultaneous A+Z is unambiguous (backward only), regardless of tick timing;moveData.triggerOn(real fire) is separately gated off while A is held, so no accidental-shot risk. User-verified fixed via a second instrumented rebuild (every notch now produces exactly one correct, symmetric step both directions). Committed384c3e7edirectly ontofix/d223-wheel-cycle(traces stripped, real fix only), pushed — PR #54 updated, CI re-running. Temp worktree removed after push. - D232 anomaly RESOLVED (2026-09-12, this session): not a bug, it's D117.
Resumed the verification per the "Next steps" above. (1) Added the
GE_NOHITFLASH_TRACEenv-gatedosSyncPrintfin the gate (pattern:getenv-gated likeGE_D160,bondview2.c:4247), rebuilt (241/241 green). (2) Ran it headless (GE_PCDUMP,-level_09, ~3500 frames, unmanned/no-input) — zero trace fires, confirming no damage ever occurs in this kind of run, so the gate is provably inert here regardless of setting — the earlier frame-28-45 "brighter" delta cannot be the hit-flash gate touching a real fade. (3) Re-ran the actual A/B properly: same binary, onlyGame.NoHitFlashtoggled viadata/ge007.ini(no env override for it), A0/A1 = two runs with the flag OFF (repeat), B = ON, each 100GE_PCDUMPframes, diffed withtools_pc/framediff.pyin both--exactand the project's own default structural/tolerant mode. Result: A0 vs A1 (same build, same config, run twice) = 0/100 frames match (87% of pixels differ by frame ~73, tolerant mode too) — i.e. the port is NOT deterministic even by frame ~73 in this scenario. A0 vs B (gate off vs on) = 99/100 frames match (tolerant mode; differences are ~0.1-0.3% of pixels, an order of magnitude smaller than the A0/A1 same- build noise floor). Conclusion: D232's gate causes no detectable visual effect beyond ordinary run-to-run noise — the prior session's "gate-on uniformly ~24 luminance units brighter" read was a false signal from an under-powered metric (mean luminance, coarse enough to miss/alias massive per-pixel divergence) colliding with the already-catalogued, still-OPEN D117 frame-to-frame nondeterminism (findings.mdD117: "root-caused;GE_DETERMdeferred... determinism NOT achieved";framediff.py's own header already documents "two runs of the same build diverge 15-40% of pixels by frame ~200" — this session's repro (87% by frame ~73) is the same phenomenon, not a new one). Lesson for future headless A/B verification on this port: useframediff.py's per-pixel/structural comparison, not a single scalar like mean luminance — D117 noise is large enough that a coarse metric can average it away and look like a false "clean" result OR (as happened here) get misread as a real signal. No code fix needed; D232 stands as code-verified correct (exhaustive caller audit, now also headless-trace-confirmed inert absent real damage) with the human playtest as the actual merge gate, same as always intended. Test junk from this pass (ppm/,ppm_A0/,ppm_A1/,ppm_B/) cleaned;data/ ge007.inirestored toNoHitFlash = 0. Still owed: file this as adocs/dev/findings.md§F row (nextDxxafter whatever lands from #55/#56) + regenerate the CSV index — deferred this session (out of scope for a "confirm testing is ready" pass; low risk to leave for the session that pushes the D232 PR, since the trace addition is a behavior-preserving#ifdef PORTdiagnostic, same class asGE_D160). - Environment gotcha #2:
timeout's TERM does not reliably kill the game on Windows (the SDL app survives it) — a zombiege007.x86_64.exethen holdsbuild-pc/ge007.x86_64.exeopen and the link fails with "cannot open output file ... Permission denied". Checktasklist | grep ge007before linking; kill leftovers of your own timed runs (confirm viaWin32_Process.CommandLineit isn't the user's session first). - Environment gotcha (cost a misdiagnosis this session): when spawning
the toolchain from an agent shell, mingw
cc1.execan fail to launch (gcc exits 1 with no output; direct cc1 run → 127; via PowerShell →0xC0000135STATUS_DLL_NOT_FOUND) even though every imported DLL exists — it's a DLL-search-PATH problem in the spawned child, not a broken toolchain (as/ldwork fine, which made it look like cc1 was blocked). Fix: prepend the bin dir for the child, e.g. run the build aspowershell -Command "$env:PATH='C:\msys64\mingw64\bin;'+$env:PATH; bash ./build-pc.sh ntsc-final". If a future session sees silent gcc failures, check this before concluding the toolchain is broken (and before blaming AV/sandbox). The same PATH prepend is needed to RUN the game exe from an agent shell (bare./build-pc/ge007.x86_64.exe→ exit 127, no output) — and note a failed build does NOT stop a&&chain when piped throughtail(the pipeline's exit status is tail's), so verify the link line actually passed before copying/renaming the exe.
M-102c (2026-09-11) — user field corrections to the roadmap: in-level music actually plays (quality issue, not absence); D228 confirmed world-model-only; D194 got real external corroboration.
Three corrections to docs/dev/notes/ROADMAP-1.0.md from the user's own
current testing, folded in:
- In-level music is NOT absent (contra
findings.md/README/GRAPHICS-BACKLOG.md, all still saying D77 = silent past intro/menu — stale, flagged in the roadmap's own footnote as a follow-up docs PR, not fixed here). The real remaining issue: some instruments on some tracks (elevator music named specifically) sound wrong, plus occasional general-play audio garbling. Needs its ownDxx— not yet created. Re-scoped in the roadmap to an audio-quality investigation (seq/ instrument-bank decode, not mixer/trigger). - D228 confirmed world-model-specific: NPCs carrying guns show white missing-texture patches on the world-model weapon geometry; the player's already-fixed 1P viewmodel isn't implicated. Narrows the search surface.
- D194 mouse feel — external corroboration, not a new mechanism.
Investigated
<games>\Emulators\Nintendo 64\1964_GEPD_Edition(the user's reference point for "feels better") — the 1964 emulator's bundledMouse_Injector.dll(closed-source, no source available; the emulator's ownsource.tar.xzis just the stock 1964 core). Its exports (HookRDRAM) and readme show it writes directly into the emulated game's RAM rather than going through N64 controller-pad emulation, and its own FAQ says responsiveness needs mouse polling decoupled from frame pacing (recommendsNtSetTimerResolution+ disabling vsync). This independently corroborates the port's own existing D194 diagnosis (part b: sensitivity couples to frame/poll rate) as the dominant lever — raises confidence in prioritizing that half of the fix, doesn't change the fix direction or supply new constants (nothing else was recoverable from a stripped, hardcoded-per-ROM-title binary).
M-102b (2026-09-11) — refreshed docs/dev/notes/ROADMAP-1.0.md (v1.0 triage), same style as the docs-triage pass below.
Rewrote the plan of record: fresh "Where we are now" superseding the stale
M-49/M-87 sections, an acceptance-bar status table, and a full
impact×effort triage of every open findings-index.csv row into
Tier 1 (blocks the bar: D77 in-level music, cutscene reliability
D148/D160/D173/D219, D194 mouse feel, D190 Linux crash, D195/D229 visual
correctness) / Tier 2 (D228 AK47 textures, the D222/D218/D220 FOV
break-set, assorted papercuts) / Tier 3 (QoL: HUD scale, rebinding UI,
per-pad tuning) / explicitly-deferred (GE_DETERM test-infra, multiplayer,
region scope — decisions not bugs). Sequenced milestones + open questions
carried forward and updated. File is gitignored (docs/dev/notes/), no
PR needed.
One finding surfaced, not fixed: findings.md row D217's status cell
still leads OPEN — root cause characterised (M-86) even though its own §H
shows the green-texture defect fixed and merged (PR #47); only the D228
white-texture residual is actually open. Same class as the D194
false-FIXED bug fixed on docs/m102-triage-fixes — worth a follow-up
edit + index regen next time that branch (or a new one) is open.
M-102 (2026-09-11) — docs triage pass. Fixed a live findings-index false-positive (D194 was silently reading FIXED), the release <version> bug, and landed the parked docs/site-cleanup patch.
Ran docs/dev/notes/PROMPT-docs-triage-session.md. Full triage table and the
scoped-not-started list are in docs/dev/notes/FOLLOWUP-docs-triage-session.md
(gitignored, local). Headline items:
- Real bug, same class as the D227/M-98 misfile that cost a session:
findings-index.csv's generator bucketed D194 as FIXED — it's an open design item — because that row's table structure was broken (2 cells, no distinct status column, so the generator's fallback keyword search matched "fixed" somewhere inside 6,500 chars of narrative). Fixed by adding a real status cell; also fixed 9 more rows stuck in the generator's "?" bucket (some had unrecognized verdict words likeANALYZED/GUARDED/DIAGNOSTIC SHIPPED/ROOT-CAUSED →/NOT A BUG, now added togen_findings_index.py's vocabulary).--checkreruns clean; CSV diff reviewed — only those 10 rows moved. .github/release-notes.md's<version>placeholders were never substituted — every published release body (v0.1.0 included) shipped the literal text.ci.yml's release step nowseds it to the tag first.- Landed the parked
UNLANDED-docs-site-cleanup-steps3-5patch: deleted the orphaneddocs/StructureGuide.md, added frontmatter to 5 Pages docs, rewrotedocs/index.md's stale "Phase 2 of 4 / no audio yet / 19-of-21" status text to match the current README (the 2 files whose hunks no longer applied viagit amwere redone by hand against current content, per the note's own instructions). - Also fixed a dead
PORT-LEARNINGS.mdreference indocs/BACKLOG.md(renamed todocs/porting-notes.mdlong ago). - Not done, scoped in the follow-up note: the findings.md monster-row
convention (>15 rows still 5,000+ chars, oldest-content-first), a full
status-accuracy sweep of all 119 rows against
§H/main, a duplicate-defect clustering pass, and thedocs/dev/*-PLAN.mdlive/historical/superseded triage. Each is a session of its own; the follow-up note has the priority order. - Changes are docs +
ci.ymlonly, not yet committed to a branch/PR as of this note — do that next if picking this up mid-session.
M-101 (2026-09-11) — green guns were a STALE BRANCH, not a bug. Green/pulsating water is real, pre-existing, filed as D229 (not root-caused).
Read docs/dev/findings.md §H "D229 — M-101".
Green weapon textures on Frigate/Surface 2: not a bug. This branch was 2
commits behind main, and those 2 commits were exactly the D217 fixes
(7278baad #47 texpool alignment, f15a12b9 #49 CI cache palette-content
key). Merged main in (f8822805); user confirms the gun is good. Process
point: check branch-vs-main before investigating any D217-family symptom
reported against a feature branch.
Green/pulsating water on IsWater levels: real, pre-existing, D229. A/B
proven — plain main with none of the D227 work shows the same green.
Mechanism partly identified: sub_GAME_7F09343C binds tile 0 and tile 1 to the
same TMEM, offsets tile 1, and cross-fades TEXEL0/TEXEL1 by a sinf()-driven
PRIM_LOD_FRAC — that sine is the pulsing, so one texunit resolves blue and
the other green. Shade path ruled out by measurement. Not root-caused.
NEXT TASK if picking up D229: the existing GE_D172 multitex probe does
not fire here (it filters on tile1.tmem != tile0.tmem, equal in this draw) —
widen it. And capture a sequence across the sine period: a single frame is
useless, frame 340 reads green on all three builds tested because of where the
sine lands. Then check skywaterimages[2]'s real format against the RGBA/16b
that sub_GAME_7F09343C re-declares.
D227 is deliberately decoupled from D229 — skyPortBeginFan now takes
allowShift and the water quad passes FALSE, leaving that path byte-identical
to pre-D227. Do not "fix" the water by shifting both tiles: that was tried live
and flattens the cross-fade into constantly-green.
bunker1 golden is now badly stale. verify.sh bunker1 reports REGRESSION
at worst_cell 55.02 on this branch — but plain main is worse at 73.24.
main's D217 texture fixes moved the pixels; the re-baseline owed since
M-83/M-84 is now overdue and is making the per-patch gate useless. Worth doing
before the next rendering change.
User environment note: their playtests run with FOV turned up
(Video.FovScale); all headless captures here ran at the ini default of 100.
Green reproduces at 100, so FOV is not required for D229 — but the black bands
at the capture edges are probably the FOV-coupled D218/D222 family, not D229.
M-100b (2026-09-11) — D227 FIXED (pending your play-test). Cause was a UNIT ERROR, and it also fixes the "sky scrolls too fast" complaint.
Read docs/dev/findings.md §H "D227 — M-100b". M-82 ("D176(a) Defect 1")
read unk20/unk24 as texel counts and baked tc as (S - fold) * 32.
They are not texels — skyRender sets them as worldpos * 0.1f and the N64
path hands them to the RDP coefficient block unscaled, so they are already in
the RDP's native S10.5 1/32-texel units. That stray * 32 caused three
symptoms tracked as separate bugs: D227's seam/starburst (overflow), the
over-tiling, and M-96's "the N64 sky is nearly static, ours races"
(g_SkyCloudOffset advances 1/32 texel per tick, not 1). Fix is
tc = (S - fold) 1:1 with the fold rebased to a whole tile period.
Committed: 3f2b1b4d (fix), a0b1de40 + f1711b59 (the investigation).
Branch land/d176a-sky-perspective-scroll, PR #43 still draft.
NEXT TASK — play-test. Both prior D227 "fixes" (M-95, M-98) passed static headless checks and then failed live, so this is not closed until played. Check Cradle, Statue and Surface: seam/starburst gone, cloud scale sensible, and the sky should now scroll ~32x slower (near-static, like the N64). Then PR #43 can leave draft.
Also worth doing: re-measure the scroll rate against M-98's ~7 px/tick
baseline — it should now be ~0.2 px/tick. Method is in §H "D227 — M-98"
(cross-correlate GE_PCDUMP=345-355:1 consecutive frames, static camera).
New probe: GE_D227V=1 dumps per-vertex S/T/w, the chosen shift k, and
the baked tc from skyPortRenderPoly. This is what measured the whole thing.
Kept deliberately: the tile-shift safety valve
(skyPortPickShift/skyPortCaptureTile/skyPortEmitTileShift) — the
observed span is 31,513 against an s16's 32,767, only ~4% headroom, so it
degrades gracefully rather than overflowing if a camera exceeds it. Uses the
RDP's own tile shifts/shiftt, which port/fast3d already implements.
M-100 (2026-09-11) — D227 ROOT CAUSE FOUND AND PROVEN: s16 texture-coordinate overflow. Fix designed, not implemented — one decision needs you.
Read docs/dev/findings.md §H "D227 — M-100" for the full record. Short
version: the seam is not a crack, not a quantization mismatch, and not
view-dependence. One sky quad spans ~30,000 texels in S and T, but
Vtx.tc is S10.5 in an s16 = ±1024 texels, so 7 of its 8 texture
coordinates overflow and wrap to arbitrary values. M-95 and M-98 each fixed
a real second-order bug; neither could touch this.
What unblocked it: the user's emulator-vs-ours screenshot pair on Surface
(docs/img/bugs/d227-surface-emulator-ref.png vs
d227-surface-starburst-ours.png). The real symptom is a radial starburst,
not a crack — which is a completely different mechanism from what M-94→M-98
were chasing.
M-99's "blocked, needs a human co-session" was wrong. The defect
reproduces headlessly and statically; M-98 had already captured it (Cradle
frame 350, d176a-cradle-tower-starburst.png) but misfiled it as unrelated
"Defect 2". D227 == D176(a) Defect 2 == the Cradle tower starburst.
GE_INPUTSCRIPT camera-pan is still a dead end, but D227 never needed it.
New probe, kept: GE_D227V=1 dumps per-vertex S/T/w from
skyPortRenderPoly (env-gated, GE_D176 style). This is what proved it.
The fix, and the one open decision. Subdivide until each sub-primitive's
S/T span is under ~960 texels, with per-sub-primitive folds (folds differing
by multiples of 64 texels are seamless under GL_REPEAT at tile width 64).
Two parts are already worked out and written up in the finding: the exact
perspective-correct subdivision math, and where to place the cuts — S is
linear in w, so cut uniformly in w; uniform-in-screen is badly wrong (on
this quad's right edge, half the screen distance carries 807 texels and the
other half 26,943). What is NOT decided is the vertex budget:
dynAllocateVertices is an unchecked bump allocator over a 50 KB/frame
pool shared with the whole scene, and pure tessellation wants ~1,200-1,300
verts (~40% of it) at this camera. Options A (pure tessellation, no fast3d
change, ~40% of the pool), B (widen the texcoord transport in port/fast3d/,
then ~4x4 subdivision suffices, ~100 verts, wider blast radius), C (hybrid).
and D — RDP tile shift, which dominates the other three and is the
recommendation. shifts/shiftt in the tile descriptor is the RDP's own
precision-vs-range knob, and port/fast3d/ already implements it fully
including the left-shift case (gfx_pc.cpp ~1783). Emit tc at 32/2^k units
and set shifts = 16 - k: 2^k more range, no tessellation, no fast3d
change, no vertex-budget cost, authentic RDP idiom. k = 5 gives ±32,767
texels, covering the measured 29,830-texel span outright — i.e. exactly the
diagnostic build that already removes the artifact, with the scale corrected
rather than left wrong. Care needed: emit the shift on the sky's tile only
(an extra gDPSetTile after texSelect), and pick k per draw from the
measured span. Ask the user before implementing.
Recheck after the fix: whether the M-96 "sky scrolls extremely fast" complaint and M-98's measured ~7 px/tick were the same bug all along (wrapped tc would make the apparent pattern move at a rate unrelated to the true 1 texel/tick scroll). Plausible, unconfirmed.
Environment notes (all still apply, plus M-99's below): GE_PCDUMP
writes to ./ppm/ relative to the cwd you launch from — run from the
repo root, not build-pc/, or the frames land somewhere you won't find them.
Backgrounding the exe with a trailing & inside a tool call kills it before
it captures; use a proper background run instead.
M-99 (2026-09-11) — human playtest: D227 seam STILL PRESENT after M-98's fold fix, AND view-angle-dependent. Headless repro blocked; needs a human co-session.
Human playtest result (this session, after M-98's fold-sharing fix was pushed): the seam is still there on Statue, and — new, sharper data point — it visibly moves/changes as the mouse/player view moves. Also: "still pretty bad" on Cradle, and Surface has the same issue. This means M-98's fold-sharing fix was real (verified via static-frame diff, findings.md §H "D227 — M-98") but is NOT the dominant thing being seen live — same pattern as M-95's wScale fix before it. Read that finding fully before touching sky.c again; don't re-derive it.
What this session ruled out, cheaply, before getting stuck:
- Cross-quad water/cloud texture-scale mismatch. Hypothesis: block1
(the "below-horizon" quad,
sp274/sp43c,skyRender's first big switch) and block2 (the cloud quad,sp94/sp4b4, second switch) are drawn from independently-computed vertex data that share the horizon boundary but never share awScale/fold — texture-scale differs by 10x between them (unk0c = rawin block1's IsWater case vsunk0c = raw*0.1fin block2, both lines confirmed by direct read). Checked via the existingGE_D176env probe: both Statue AND Cradle reportIsWater=0. ForIsWater==0, block1 is a flatgDPFillRectangle(sky.c~874-897) — no texture, no scroll, can't produce a texture-pattern seam. This theory doesn't apply to either level actually tested; still untested on anIsWater==1level (candidates: Frigate, Runway, Depot, Surface1/2 — check withGE_D176=1first) where block1 WOULD go throughskyPortBeginFan/skyRenderTritextured and could genuinely conflict with block2's independent fan. Worth ruling in or out properly before the next session assumes it's dead. - Headless camera-pan repro is NOT wired up. Tried to reproduce
"changes with view" headlessly via
GE_INPUTSCRIPT— both sustained analog-stick tokens (SRIGHT) and repeated C-button pulses (CRIGHT/CUPetc.) across a 90-tick window produced zero visible camera rotation in captured frames (same look direction throughout, confirmed by identical HUD/weapon-arm framing). The real in-game mouselook almost certainly injects yaw/pitch through a pathGE_INPUTSCRIPTdoesn't touch (it only emulates the N64 controller: buttons + one fake analog stick) — worth checkingport/src/input.c's actual mouse-delta injection point for a debug hook, or adding one (env-gated, e.g.GE_FORCEVIEW=yaw,pitchset once per frame) if this investigation needs more headless angle sweeps. Until that exists, any view-angle-dependent visual bug in this game needs a human playtest to chase, full stop — don't burn another session's budget re-tryingGE_INPUTSCRIPTstick/button tokens for camera turn, they don't do it.
What's genuinely still open, ranked by how promising it looked before getting stuck on repro:
- Cross-block (water-quad vs cloud-quad) independent fan state, on an
IsWater==1level. Not ruled out yet (see above) — check first, it's cheap (oneGE_D176run per candidate level) and if true it's a clean, scoped fix (extendskyPortBeginFan's shared state to span both blocks in one frame instead of resetting between them — needs restructuring since block1 draws before block2's vertices even exist; the two calls would need to become "begin/accumulate/begin-again-without-reset/end"). - Frame-to-frame popping at the fold boundary, not a same-frame spatial
seam.
skyPortRenderPoly's fold recomputes fresh every frame from the current scrolling min-S; each time it crosses a 64-texel boundary the chosen fold jumps by 64 — should be invisible (GL_REPEAT period 64) but was never independently verified to actually be seamless in fast3d's real texture-wrap implementation for this specific combiner setup. A static-camera, long,step:1capture bracketing a known fold-crossing tick (derivable from the ~7px/tick rate measured in M-98) would show this directly if real, and — importantly — this one doesn't need view rotation to reproduce, just enough elapsed ticks, so it's still headlessly testable. Good first move for next session if a human isn't available yet. - The clip-shape switch's discrete vertex-count/topology changes as
screen corners cross the horizon (
skyRender's 16-case switch reruns every frame from scratch; adjacent cases can hand block2 a differently shaped fan, not just moved vertices) — could read as "the pattern changes as you look around" without being a same-frame crack at all. Not investigated.
Concretely blocked on: a screenshot or precise repro detail from a human — where on screen, at roughly what look angle/pitch, which level. Asked for this at end of M-99; if it's in hand for the next session, START there instead of re-running the ranked list above blind.
Also outstanding, unchanged: D228 (AK47 white missing-texture patches, different mechanism from the already-fixed D217 green-palette bug) is still fully unstarted — reasonable fallback task if the seam investigation needs a human and one isn't available yet.
Environment notes for whoever picks this up:
- Launching
ge007.x86_64.exedirectly (not viaverify.sh) needsexport PATH="/c/msys64/mingw64/bin:$PATH"first or it exits instantly (code 127, no output) — DLL resolution, not a real crash. run_in_background: true+taskkill //F //IM ge007.x86_64.exeonce you have the frames you need is the normal pattern for a capture — the background task will report "failed" (nonzero exit from the forced kill) even on a fully successful capture. That's expected, not a bug.tools_pc/ppm2bmp.py in.ppm out.bmp <scale>needs an integer scale arg or it silently no-ops and prints usage — easy to lose a few minutes to.
M-98 (2026-09-11) — D227 seam residual FIXED (fold-sharing) + scroll-rate quantitatively measured
Picked up the exact "left for next time" from M-97: re-derived the intended sky scroll rate instead of re-attempting the seam blind.
Ruled out: the global tick rate. skyTick() (verbatim N64 logic) adds
g_ClockTimer (~1/tick, per D193/D204) to g_SkyCloudOffset once per logic
tick — not per-render-frame, not double-counted at sub_GAME_7F097388's
call sites either. A global timing bug would speed up everything, not just
the sky, so it isn't this.
Found + fixed: M-95 shared wScale across a whole sky fan but left
skyPortRenderPoly's S/T fold (foldS/foldT) computed per-call — the
exact same bug class, one field over, never touched. Two triangles sharing
a fan vertex could floor its S/T to different multiples of 64 texels;
nominally invisible (GL_REPEAT period 64) but each triangle bakes
(S-fold)*32 into its own s16 buffer, so two folds 64 texels apart = tc
values 2048 apart — a real crack. This is why M-96's playtest still saw the
seam after M-95. Fixed the same way as wScale: skyPortBeginFan() now
also computes one shared fold, threaded into skyPortRenderPoly.
#ifdef PORT only, src/game/sky.c, no game-logic touch.
Verified: build clean; -level_22 frame 345 (same frame M-95 used) —
fresh capture shows no visible crease at the ~x=1000/1280 location that's
clearly cracked in docs/img/bugs/d227-sky-seam-statue.png; verify.sh bunker1 PASS, worst_cell unchanged (known stale-golden-baseline number,
not a regression). No new screenshot committed — didn't have an M-96-era
"still broken" capture to diff against.
Scroll-rate measurement (the actual ask): 11 consecutive frames
(GE_PCDUMP=345-355:1, static camera) cross-correlated on a sky-band row →
consistent ~7 px/tick at 640px capture width, i.e. a full screen-width
sweep roughly every 90 ticks (~1.5s @60Hz). Hard number confirming
"extremely fast," not just a subjective read. Root cause of the magnitude
itself is NOT found — the accumulator and vertex geometry feeding it are
unmodified N64 logic, so if it's genuinely wrong the bug has to be in how
many screen pixels the D176(a) Path B substitution maps one texel-tick to
(camera/FOV/projection-matrix scale), not in the tick-rate accumulator.
Next step: compare the recovered per-pixel S gradient within one frame
against what the N64 #else edge-setup math (130.0f/arg4-driven) computes
for the same triangle — full spec in findings.md §H "D227 — M-98".
Environment gotcha hit this session: launching ge007.x86_64.exe
directly (not through verify.sh, which already has this) needs
export PATH="/c/msys64/mingw64/bin:$PATH" first, or the process exits
immediately with code 127 (DLL resolution) and prints nothing.
Cradle side of PR #43 checked this session (was outstanding since M-97):
verify.sh cradle PASS, no crash/regression from the fold fix. But found a
much more visually dramatic pre-existing artifact while there: looking
up the suspension tower near spawn, ~half the sky renders as a sharp
radiating "starburst" instead of clouds — screenshot
docs/img/bugs/d176a-cradle-tower-starburst.png. Confirmed pre-existing
(pixel-identical with sky.c reverted to pre-M-98) and confirmed static
(pixel-identical 30 ticks apart, not the D227 "moves at different rates"
symptom) — this is the already-known, already-open D176(a) Defect 2
(horizon-band tessellation), just now with a concrete repro instead of
"inconclusive." Not fixed — real tessellation work, separate task. Full
write-up: findings.md §H "D176(a) — M-98".
Not done: human eyeball still owed on the Statue seam-close, the scroll-speed question, and this new Cradle starburst screenshot before PR #43 comes off draft. Defect 2 itself (Cradle starburst) not attempted — next session candidate if D227/scroll-speed get closed out first. D228 (AK47 white texture patches) still unstarted.
M-97 (2026-09-11) — PR triage/merge pass + D227 human playtest (NEGATIVE result)
PR triage (fastest→slowest, per the new docs/dev-process.md §6 policy):
merged #48 (D219 doc), closed #26 (stale — D191/D193/D149 all
superseded by since-fixed/verified findings, real conflicts), merged #51
(Pages cleanup, folded the new §6 policy doc in rather than a 9th PR), merged
#52 (README cleanup — also corrected a real staleness bug found in
review: README/LEVEL-STATUS still said "19/21, two levels crash" when D191
and D193 are both FIXED+verified — reworded to "no known crashes, full
21-mission re-confirmation still owed" instead of either the stale claim or
an unverified "21/21" overclaim), merged #50 (D221 — human mouse-playtest
on SELECT FILE: confirmed pass, all slots + Copy/Erase, no crash).
PR #43 (D176a/D227 sky) — human playtest: FAILED, stays in draft. Built
this branch, launched Statue (-level_22) directly. Result: the D227
seam/crack is still visible despite the M-95 wScale-sharing fix, which
directly contradicts that commit's "closes the visible seam" claim — M-95
itself had flagged the temporal "moves at different rates" theory as
never re-verified (only the static-frame quantization theory was actually
checked), and this playtest is that missing check, landing negative.
New data point, not previously documented: the sky's scroll rate should
be near-static on N64 and is currently extremely fast on this build —
stronger than M-93's "still reads too fast," and possibly the same
wrong-magnitude root cause behind both the speed complaint and the seam
(a wrong per-frame tc delta could produce both). Next investigation:
re-derive the intended N64 sky scroll rate from sky.c/ROM env data and
check what the port's actual per-frame tc delta computes to — do not
re-attempt the seam fix in isolation from the speed question. Full
write-up: findings.md §H "D227 — M-96".
Not done this session: the Cradle side of PR #43 (only Statue was checked), PR #47's weapon-texture-matrix playtest, PR #49 (blocked on #47).
M-91 (2026-09-15) — D217 defect ① ROOT CAUSE FOUND + FIXED (M-90). Texpool arena base misalignment on PC; one-line-class fix in texInitPool; FACILITY/BUNKER1/DEPOT verified clean. 21-level sweep: 21/21 PASS.
M-91 continued (same session, later) — D219 re-checked (PR #48), defect ② swept + PR'd (#49), D221 root-caused + fixed
- D219 runtime re-check — DONE. Scripted-fire repro (
GE_INPUTSCRIPTZ-pulses on-level_33,GE_D172=1+ PPM dump): 40/40 particle multitex tris sample the correct RGBA fire tile (tmem=392), zero BUG/unknown-GBI/D146-abort lines, 0 purple/magenta pixels across 28 frames. Mechanism covered by the D172 fix on currentmain; final close pending a human by-eye pass at a live Dam PPK firefight (folds into the PR #47 playtest). Docs-only: PR #48 (docs/d219-runtime-recheck, non-draft). - D217 defect ② (CI-cache content key) — swept + PR'd. The stale
fix/d217-model-texture-palettebranch was abandoned; its one commit (f6fc0399) cherry-picked onto freshfix/d217-ci-cache-content-key. Full sweep on that branch: 21/21 PASS. PR #49 (draft — rebase + re-sweep after #47 lands so the pair is tested together). - D221 (SELECT FILE mouse hit-box) — ROOT CAUSE FOUND + FIXED, headless-verified, swept, PR'd. It was NOT a coordinate/origin mismatch.
interface_menu05_fileselectpasses four separate f32 locals (&xmin, &ymin) toprojectRectCornersTo2D, which takes{min,max}coord2d pairs — relying on the N64 compiler laying each max adjacent to its min. PC MinGW lays them out in declaration order, soarg1->f[1]read ymax-as-X andarg2->f[1]read an unrelated stack float (observed2.25734, laternan) → the vertical hit band collapsed to a ~14 px strip at each wallet's bottom edge (sometimes NaN = unselectable). Exactly "click below the file." Fix: explicit{xmin,xmax}/{ymin,ymax}pairs under#ifdef PORTinfront.c(N64 call untouched; ABI/layout exception per rule 2). Controlled headless test: same cursor pos + A-press — pre-fix never selected, post-fix selects folder 0 and advances. Full write-up: findings.md §H "D221 — M-91". PR #50 (fix/d221-fileselect-hitbox, draft; sweep on branch: 21/21 PASS; probe stripped). - D220 (muzzle-flash FovScale drift) — ANALYZED, no bug found. The flash nodes share the gun DL and the (scaled) per-frame projection → attachment is geometrically preserved; the whole camera-anchored viewmodel migrates toward screen center as FOV widens (correct perspective). Disposition: human call — QoL viewmodel re-anchor if it bothers (VIEWMODEL-RESEARCH family), not a fidelity fix. Index row updated (commit 2 of PR #50).
- Open / human-gated: PR #47 weapon-matrix playtest (+ Dam PPK by-eye closes D219); PR #43/D176a sky eyeball; PRs #49/#50 await review/merge (#49 rebase + re-sweep after #47). All three M-87 QA items (D217①, D220, D221) now have resolutions pending human eyeballs. Next AI task when resumed: nothing blocking on the v0.2.0 gate is left un-investigated — remaining gate items are all human-gated; after merges: rebase #49, re-sweep, then tag/release per ROADMAP-1.0.
The M-89 stale-arena-pointer theory is REFUTED and the real cause is found, fixed, probe-verified. Full write-up: findings.md §H "D217 — M-90" (read that before touching D217 again). One paragraph version: texAlignIndices pads CI index rows to absolute 8-byte boundaries, and the model-DL G_LOADTLUT palette offset is a formula that only matches reality when the texpool base is 8-aligned. On N64 the memp bump allocations feeding these pools happened to be 8-multiples; PC pointer bloat (D36/D37-class struct growth) made some STAGE-bank sizes not, so on FACILITY both weapon-pool bases land %8==4 → every palette in those pools sits 4 bytes early → the grip's count=1 TLUT reads the next tenant's header slot (texnum=26 → LE s16 → 0x1A00 = the green). The same mechanism explains M-86's "leading zero pad" tell on larger loads (palette 2 entries early ⇒ first two read entries are zero pad). BUNKER1 was clean only because its pool bases happened to be aligned.
Fix landed (working tree, uncommitted): src/game/image.c texInitPool, #ifdef PORT: round the arena base up to 8 before publishing start; end unchanged (pool loses ≤7 bytes). No-op on N64. ABI/layout-class edit per AGENTS.md rule 2's narrow exception — no logic touched.
Verified so far:
./build-pc.sh ntsc-finalclean; no new/duplicate symbols.- FACILITY
-level_34: 188/188 decodes %8==0 (was 30 at %8==4); draw probe readspal0=0001from the correct slot (was1a00); 0 exact-green pixels in PPM frames 250–450 (pre-fix ~1300 in 300–410), 0 strong-green. - BUNKER1
-level_09: still clean (183/183 aligned, 0 CHANGED draws, 0 green). - DEPOT
-level_30: clean (229/229 aligned, 0 green in window). - Full
verify.sh sweep: 21/21 PASS (log:sweep_d217.logat repo root, scratch).
Probes still in tree (strip before commit, or keep one session for the playtest follow-up): GE_D217TEX decode probe extended with a D217TEX-SIZE line (src/game/image.c texInflateZlib); D217TEX-DRAW change-detection probe in port/fast3d/gfx_pc.cpp gfx_dp_load_tlut. Both env-gated, inert by default.
Next steps (in order):
Read— done: 21/21 PASS, no CRASH/REGRESSION/STALLED.sweep_d217.log- Commit: the
texInitPoolfix + findings/HANDOFF updates (strip probes first unless keeping them for step 4). Suggested branch namefix/d217-texpool-alignment. - Human-gated: user playtest eyeball of the M-87 weapon matrix (PP7/PPK/AK47/M16 grips+bodies, watch face, NPC-held guns) — hand off with a short list of what to look at; this closes D217 defect ①.
- Defect ② (
fix/d217-model-texture-palette@f6fc0399, CI-cache content hash) still parked, still owes its own sweep — separate PR when it gets one. - Rest of the v0.2.0 gate unchanged: PR #43 (D176a sky) draft + human eyeball on
-level_22; D219 scripted-Dam-explosion runtime re-check; then tag/release per ROADMAP-1.0.
Unchanged: Tier 2 cluster untouched. Environment notes from M-90 still hold (MSYS2 PATH, tasklist check before "build broken" if ld.exe: Permission denied).
M-90 (2026-09-11) — session cut short (usage limit). D217 probe extended + built; a local-Qwen triage's specific citations were caught WRONG on hand-verification (see below); the underlying mechanism was independently re-derived and looks solid. NOT yet run to completion — next session picks up at "read the run.log", not at square one.
Housekeeping done: untracked docs/dev/MODERN-PORT-FEATURES-BACKLOG.md (from a concurrent session) moved to docs/dev/notes/ (gitignored) per user instruction — do not treat this as new work, it's someone else's in-flight doc.
Local-Qwen dispatch result — DO NOT TRUST WITHOUT RE-VERIFYING. Dispatched triage to investigate the D217 "stale-arena-pointer" lead (per M-89's exact spec). It came back HIGH confidence with specific citations: "texWriteLoadToTmemAddr's only call site is tex.c:918" and "texFindInPool has only two callers repo-wide (tex.c:405, image.c:2443)". Both are false — direct grep shows texFindInPool is also called at tex.c:856 and tex.c:920 (same file it claimed to have fully searched), and tex.c:918 is actually texnum2 = (in->words.w1 >> 12) & 0xfff; inside texLoadFromGdl's TEXTURETYPE_DETAIL case — not a texWriteLoadToTmemAddr call at all. This is the exact "confidently fabricates" failure CLAUDE.md already warns about. Its overall shape (address baked once into a GDL at model/room-load time, reused pool) pointed in a directionally-useful place, but every specific line citation from it needs independent re-derivation, which is what the rest of this session did by hand.
What was independently verified by hand-reading the real code (trust this part):
texWriteLoadToTmemAddr(tex.c:494) emitsgDPSetTextureImage(gdl++, tex->gbiformat, depth, 1, tex->data)—tex->datais a realu8*(image.h:42), andgSetImage's packing macro (include/PR/gbi.h:3106-3111) stores it as(uintptr_t)(i)— a full 64-bit store, NOT a 32-bit-truncated N64 physical address. This rules out a D3x-class pointer-truncation bug for this path — a theory I formed and then killed myself before wasting a build cycle on it. Don't re-open this angle without new evidence.texLoadFromGdl(tex.c:779) — which bakes those addresses into a display list — runs only at object/room LOAD time: callers areobjecthandler_2.c:82(model load) andbg.c:2499/2601/2603(room primary-GDL load, confirmed by the doc comment atlightfixture.c:31— "Room load ... texLoadFromGdl() walks the room's display list"). It does not run per-frame.gfx_dp_load_tlut(port/fast3d/gfx_pc.cpp:2202) — the draw-time consumer — does a live re-read of memory at the baked address on every single draw call (base = (const uint16_t *)rdp.texture_to_load.addr + ...; *dst++ = PD_BE16(*src++);, loop at line 2230-2234). It is NOT cached across frames; it always reflects whatever bytes currently live at that address.- Conclusion (HIGH confidence, own derivation): the mechanism is real and matches the symptom shape exactly — a texture's address is baked into a model/room's DL once, at load time; every subsequent frame's draw re-reads that literal address fresh. If some OTHER later texture decode reuses the same
texpoolarena bytes (bump allocator,image.cleftpos/rightpos, no per-texture free/refcounting — confirmed atimage.c:2323-2328,2508-2525) before the first model's DL stops being drawn, every frame after that point reads the new tenant's bytes — permanently, matching "solid green from a fixed point onward, never intermittent." This does not require N64/PC to differ in the C code (AGENTS.md ground truth holds) — the divergence must be in pool sizing/reuse cadence between platforms, not logic.
Probe extension shipped this session (built, NOT yet run to a read result):
src/game/image.c(texInflateZlib,#ifdef PORTGE_D217TEXblock) — addedaddr=%p(thedstpointer, i.e.tex->data) to the existing decode-time log line.port/fast3d/gfx_pc.cpp(gfx_dp_load_tlut) — added a newGE_D217TEXlog (D217TEX-DRAW addr=%p pal0=%04x pal1=%04x count=%u, viasysLogPrintf, NOTosSyncPrintf— that doesn't link intoport/fast3d, cost one failed build to learn) logging a live re-read of the palette bytes at draw time, keyed by address (texnum isn't available at this layer — correlate the two logs byaddr=, not by texnum).- Both builds clean (
./build-pc.sh ntsc-final, MSYS2 MINGW64 PATH). Neither is applied/committed yet — working tree only. - Hit one environment snag: the build failed once with
ld.exe: cannot open output file ... Permission denied— a leftoverge007.x86_64.exe(PID from an earlier/concurrent run) had the file locked. User approved killing it; by the timetaskkillran the process had already exited on its own. If this recurs, checktasklist //FI "IMAGENAME eq ge007*"before assuming the build is broken.
Exact next step (do this first, it's cheap): tools_pc/verify.sh was run once (GE_D217TEX=1 ./tools_pc/verify.sh facility, PASS, 3 frames) but the session ended before locating/reading the resulting log. verify.sh writes the run's stdout+stderr to "$capdir/run.log" (see tools_pc/verify.sh:184, CAPDIR="$capdir" set at line 231) — find that capdir (likely under a tools_pc/-relative temp/capture dir printed by verify.sh itself, or grep verify.sh for how capdir is constructed if not obvious) and grep it for D217TEX lines. Correlate the decode-time D217TEX texnum=... addr=... lines against the draw-time D217TEX-DRAW addr=... lines by address: if any addr shows a different pal0 between its most-recent matching decode-time line and a draw-time line that comes after it, that is direct proof of the stale-arena-pointer mechanism, and narrows the fix to whichever call site re-runs texInitPool/reuses that arena region while an earlier consumer (a still-drawn weapon or room model) is still alive. If the addresses never collide in this FACILITY run, extend the capture window or try the exact BUNKER1-vs-FACILITY pairing from the M-89 decode-only probe (frame ~300, post-vent-drop-cutscene) with both probes active together.
Not done this session, unchanged from M-89: PR #43 (D176a sky) still draft, still owed a human eyeball on -level_22/Cradle. D219 still needs the scripted-Dam-explosion runtime re-check. Tier 2 cluster untouched.
M-88/M-89 (2026-09-10/11) — v0.2.0 fidelity push. PRs #39-#42, #44-#46 all merged. D77 CLOSED. PR #43 (D176a) draft, CI-green, owed a human eyeball. D217 still open — two theories refuted this session, new stale-arena-pointer lead for the next session. D219 triage note added (likely already fixed by D172, needs a runtime re-check).
D217 status for the next session, read this before doing anything else on
it: two full theories are now refuted with hard evidence (not just
inspection) — (1) the offline sidecar (tools_pc/d43_emit.py) never touches
texture bytes (M-88); (2) the gfx_dp_load_tlut pitch/address math is
provably correct, AND (empirically, PR #45, merged) the decode itself
(texInflateZlib) produces byte-identical palette content for the affected
texture (texnum=1608, the PP7 grip) on both BUNKER1 (correct) and
FACILITY (wrong/green) — proving the bug is strictly downstream of a
correct decode. The already-written, previously-untested-against-this-repro
parked fix fix/d217-model-texture-palette (CI-cache content-hash, defect
②) was built and run directly against the FACILITY repro and still shows
green — so defect ① is NOT the same bug as ②, don't expect ②'s merge to
fix this. Current best lead (untested): a stale/dangling tex->data
pointer into the texpool arena — probe the draw-time address vs. the
most-recently-decoded content at that address (spec + probe extension
notes in findings.md D217, search "stale-arena-pointer"). A GE_D217TEX
env-gated probe is already in tree (src/game/image.c) logging
decode-time texnum/palette[0..1] — extend it with an address field
rather than starting over.
Goal for this arc: ship v0.2.0 in the next few days, fidelity-only scope
(graphics + audio 1:1 with N64; hard QoL out of scope — see
docs/dev/notes/ROADMAP-1.0.md "M-87 triage" section, which IS the priority
order). Tier 1: D217 (weapon/NPC texture palette) > D77 (in-level music
missing, hard-gates UNLOCKED-FPS-PLAN/WIDESCREEN-FOV-PLAN) > D176(a) (sky).
Tier 2 (after Tier 1): D222+D218+D220 FOV-slider break-set, D148/D160/D173
cutscene actors, D219 purple explosions. QoL items (D194, D223, D224, D225,
D226, D211-as-degrees, D218-as-slider) are explicitly OUT OF SCOPE this pass.
Usage note (mid-session): user flagged we were at ~70% of the cloud
usage limit; killed one just-started cloud subagent (D217 follow-up) with
near-zero loss, paused further cloud subagent dispatches, and switched to
the local Qwen delegate (delegate-local MCP, cost-free) for further D217
work once its backend (LiteLLM proxy + Unsloth Studio) was confirmed up.
If picking this up in a new session, check the local backend first
(local_backend_status()) — it was down for a stretch this session, up by
the end.
Landed this session:
- PR #39 merged (
e117c0d7) — golden bunker1 re-baseline (D215) + M-85 D217/D211 triage docs. Docs+PNG only. - PR #40 merged (
531e24e1) —tools_pc/verify.shfast-kill (aa199729 picked up from a prior local branch): kills each level run as soon as its lastGE_PCDUMPframe lands instead of idling the full watchdog. Verified locally:bunker119.5s (was ~45s). Makes every subsequent verify faster. - PR #41 merged (
5e111f0d) — docs-only: proves D217 defect ① is NOT atools_pc/d43_emit.pysidecar bug (full texture-reference-chain trace +GE_TEXDUMPreproduction). New lead:port/fast3d/gfx_pc.cpp'sgfx_dp_load_tlutpalette-address/pitch math ×src/game/tex.c'stexWriteLoadToTmemAddrliteralwidth=1SetTextureImagecall. - PR #42 merged (
114d1edd) — D77 CLOSED. In-level music was silent becausesrc/game/mp_music.c'ssub_GAME_7F0C0BF0()had noreturnstatement (D6-class UB, same as D187) — every in-level music-start trigger fed a garbage/zero volume intomusicTrack1ApplySeqpVol/Track3right beforePlay(), silencing the track while note-on processing continued normally underneath (which is why it looked like a deeper pipeline bug). Front-end/menu music bypasses that path entirely, hence unaffected. Fix: one line,#ifdef AVOID_UB. Verified with a new[AUDIOLVL]peak/RMS probe (port/src/audio.c,GE_AUDIOTRACE-gated): before fix, 29/29 samples over a 28s BUNKER1 capture readpeak=0 rms=0.0despite 173 note events firing; after fix, every sample is sustained non-zero. This was the hard gate forUNLOCKED-FPS-PLAN.md/WIDESCREEN-FOV-PLAN.md— they can be picked up again now (still Tier-2-and-later work, not this pass). Owed: a human by-ear BUNKER1 pass to confirm audible (headless capture only proves non-zero PCM reaches the SDL queue).
PR #43 (draft) land/d176a-sky-perspective-scroll — D176(a) sky
scroll/tiling too fast, and Cradle's sky was black at default FOV even
after Path B (PR #18). Root cause: skyPortRenderPoly placed sky-quad verts
under a plain w=1 ortho projection, so fast3d interpolated tc linearly
in screen space; the source S/T is computed upstream without a perspective
divide (RDP G_TRI_SHADE_TXTR-equivalent), and a quad's near-vs-horizon
vertices can differ ~20-38x in camera-space w — linear interpolation
smears the whole S/T delta evenly across every pixel instead of weighting it
toward the near vertices. Fix: a custom projection matrix gives each vertex
its real camera-space w (already computed upstream, unk0c, previously
unused) while keeping screen-space x/y unchanged, so the GPU's normal
perspective divide does the right thing — same pipeline every other textured
draw already relies on. CI green, kept draft per project convention
(rendering fix, no display access to fully confirm) — owed a human
eyeball on -level_22 (Statue) or Cradle (-level_41): does the cloud
scroll read like a reasonable slow drift now, not a fast/aliased hatching?
Defect 2 (horizon-band tessellation) and the separate FOV-coupled
black-sky-past-frustum-edge symptom (D218/D222-family) are untouched.
Still parked, do NOT lose track of it: branch
fix/d217-model-texture-palette (local only, commit f6fc0399) — a REAL
but INSUFFICIENT fix for D217 defect ② (CI-texture-cache palette-content
collision; FNV-1a hash added to the cache key in port/fast3d/gfx_pc.{cpp,h}).
Does not fix the visible symptom (that's defect ①, in progress below). Owes a
full 21-level verify.sh sweep before it can merge — per user instruction,
reserve that sweep for right before cutting v0.2.0, don't run it now. Land
it then, or fold its diff into whatever the D217 defect-① fix needs.
D217 defect ① — worked directly (not delegated) after two local-Qwen
dispatches (see PR #44/#45 above) supplied leads that turned out wrong on
inspection. Summary of the chase, in order: (1) d43_emit.py sidecar —
ruled out (PR #41). (2) local-Qwen proposed hardcoding gfx_dp_load_tlut's
pitch=1 — hand-derived and refuted, it's a no-op given the GBI width-1
packing (PR #44). (3) built the GE_D217TEX decode-time probe myself,
ran it on BUNKER1 vs FACILITY — decode is byte-identical across levels,
ruling out a ROM/asset-table issue too (PR #45). (4) built + ran the parked
defect-② fix directly against the FACILITY repro to test whether ① and ②
are the same bug — still green, they are NOT the same bug (also PR #45).
Current best lead: a stale/dangling tex->data pointer into the shared
texpool arena, resolved once and not re-checked at draw time (full spec
in findings.md D217, search "stale-arena-pointer") — nobody has probed this
directly yet, that's the next session's starting point.
D219 (Tier 2, purple explosions) — dispatched a local-Qwen triage ahead
of schedule since it was free and idle-time-shaped. Static analysis found
no separate/unfixed code path from the already-fixed D172 mechanism;
plausibly already fixed, needs a runtime re-check with a scripted Dam
explosion + GE_D172=1 (not yet built). Docs-only note landed, PR #46.
Environment note confirmed this session: this session CAN build
(./build-pc.sh ntsc-final from Bash with /c/msys64/mingw64/bin prepended
to PATH) and run tools_pc/verify.sh <level> for a single targeted level —
consistent with env-no-runtime-verify.md memory (interactive sessions can).
Did not run a full sweep (correctly deferred per user instruction).
Not yet started: Tier 2 cluster (D222/D218/D220 FOV break-set, D148/D160/D173 cutscenes) — pick up after Tier 1 lands, per ROADMAP-1.0.md M-87 triage order. D219 got a head start (triage note above, still needs a runtime re-check, not a real fix session yet). If Tier 1 alone doesn't fit the time budget, ship v0.2.0 with whatever's landed/verified and hand off the rest (explicit user instruction — don't stretch the release for Tier 2).
Session end state (M-89): Tier 1 = 1 of 3 fully closed (D77), 1 of 3
CI-green-draft awaiting a human eyeball (D176a, PR #43), 1 of 3 still open
with a concrete next probe (D217). Everything landed is on main; nothing
is stuck in a stale worktree or unpushed branch except the deliberately
parked fix/d217-model-texture-palette (sweep-gated, see above) and
docs/v0.1.0-completion-count-fix/chore/agentic-speedups-s1-4-6/
fix/linux-stan-linktile-ptr-trunc (pre-existing local branches, unrelated
to this session, not investigated — check their status before assuming
they're still relevant).
M-83 (2026-09-09) — Agentic session. THREE PRs (all CI-green, all off main): #36 QoL wave, #37 D215 viewmodel fix, #38 D216 SkipIntro.
Session env: could build (MSYS2 MINGW64 toolchain on PATH), run
tools_pc/verify.sh (win), and capture GE_PCDUMP frames — NOT display-blind
like M-82. Could NOT drive in-level gameplay (no scripted playthrough), so
feel-checks are still owed to a human.
| PR | Branch | State | Contents |
|---|---|---|---|
| #36 | feat/pc-qol-fov-aniso-fps |
ready, CI green | D211 Video.FovScale (FOV slider, WIDESCREEN-FOV-PLAN.md Phase 4) · D212 Video.Anisotropy · D213 Video.DisplayFPS · D214 [Bind] keyboard rebinding. All port-only, default = byte-identical. |
| #37 | fix/d215-viewmodel-renderdata-flags |
draft (owed 1 eyeball), CI green | D215 — the "no gun visible while playing" bug. |
| #38 | feat/d216-skip-intro |
ready, CI green | D216 Game.SkipIntro → boots to the SELECT FILE menu. |
D215 detail. gunRenderFirstPersonGunModels seeded ModelRenderData via
*(ModelRenderData *)&D_80035CC0 — a reinterpret across two separately-declared
adjacent globals (u32 D_80035CC0=0 then u32 D_80035CC4[]={1,3,0,...}, gun.c)
that only lands right on the N64 linker + 4-byte pointers. On x86-64
renderdata.flags read 0, gating every weapon-model DL node off in
modelRenderNodeGundl (flags & 1) → weapon subdraw emitted 1 gfx cmd not
~150. All 3 VIEWMODEL-RESEARCH.md suspects ruled out first by a new GE_DVM=1
probe. Fix = #ifdef PORT explicit field init (zbufferenabled=TRUE; flags=3).
Headless-verified: flags 0→3, subdraw 1→153 cmds, bunker1 framediff
worst_cell 12.2→28.5, 6-level sweep crash-free. GE_DVM probes left in
(env-gated). VIEWMODEL-RESEARCH.md retired.
Testing / merge notes for the next session:
- Branches are independent off
mainand don't conflict in code. To test all three together, make a scratch integration branch (git mergeall three onto atest/m83branch) — the only conflicts are 1-linefindings.md/findings-index.csvrow-adjacency near| D85 |(keep all rows). - D215 owes a golden re-baseline: the gun now renders in the
tools_pc/goldenbunker1 frames, soverify.sh bunker1reports REGRESSION until the golden is regenerated. Do that only after the gun is eyeball-confirmed correct. #37stays draft until someone boots BUNKER1 and sees the gun.- The working
data/ge007.inihas non-default values left by interrupted verify.sh runs (MSAA=8,TextureFilter=2,ScreenShakeIntensity=10,MouseAimSpeed=16,PadTriggerPct=23) — delete the file or reset in F10 if a test looks off; not from the PRs.
Owed feel-checks (in-level, human):
- D211
Video.FovScale≠ 100 (F10 "FOV scale %"): world widens uniformly, HUD unmoved, aim-zoom composes, no stretch in a 4:3 window. - D212
Video.Anisotropy1 vs 16 on a grazing surface (Dam walkway / Depot roof). - D213
Video.DisplayFPS: reads ~30, no HUD overlap. - D214: a non-default bind takes (
Input.Bind.Fire = Left Alt); ESC still cancels menus; a fresh ini shows the whole[Bind]block. - D215: BUNKER1 — gun visible + correctly posed.
- D216: SELECT FILE menu is fully interactive after the skip (cursor, open a slot).
Docs landed on the branches: findings.md D211–D216 rows + index regen;
WIDESCREEN-FOV-PLAN.md Phase 4, QOL-INVENTORY.md, GRAPHICS-BACKLOG.md,
VIEWMODEL-RESEARCH.md, GE-ENV-PROBES.md (GE_DVM) all updated.
M-84 (2026-09-09) — user test pass on the M-83 PRs. D211 refixed. D213/D214/D216 verified. D217/D218 logged.
Session env: interactive — user built + ran + playtested in-level. Could not drive gameplay from here; user did the eyeballing.
Test branch: test/m83-all = main + #36 + #37 + #38 + M-84 commits
(throwaway; delete once the PRs merge). The M-84 FOV refix + findings were
then folded into PR #36 and pushed (01376660); a PR comment flags the
new #ifdef PORT scope.
Results
| Item | Verdict |
|---|---|
| D216 SkipIntro | PASS — boots to SELECT FILE, menu interactive |
| D213 DisplayFPS | PASS — top-right FPS counter, clear of HUD |
| D214 rebinding | Code PASS (alt/ctrl). Doc bug fixed: section is [Input.Bind] not [Bind] (config.c groups by the last dot), or use a fully-qualified Input.Bind.Fire = line. Watch-pause menu still Enter-only for back (pre-existing, options.c, minor — small port-side add). |
| D212 aniso | PASS (M-83 already) |
| D211 FOV | REFIXED. Original fast3d gfx_apply_fov_scale only scaled pure perspective loads; the world uses field_10E0 = CPU-combined proj×view (bondview2.c:8224) → guard-rejected → only viewmodel/sky changed. Moved to game-side fovy scale at fr.c guPerspectiveF chokepoint (#ifdef PORT, frFovY *= portFovScale, clamp 160°), gated != LEVELID_TITLE (front end shares the path). fast3d hook removed; video.c sets f32 portFovScale. User-verified: menu unaffected, in-level widens. Nits owed: HUD-static + aim-zoom eyeball at ~120. |
| D215 viewmodel | Gun draws ✓ but textures garbled → split to D217. #37 still draft; flip to ready (D215 itself is done). Golden re-baseline owed after merge. |
New findings
- D217 (OPEN) — 1P viewmodel + 3rd-person Bond model textures render garbled (green/blue/yellow); Bond's watch face too. Stale-palette CI-texture bug, D161 family; surfaced by D215, not caused by it. Next:
GE_TEXDUMPon a weapon texture in BUNKER1; checkgDPLoadTLUTreachesgfx_pc.cppfor model DLs. - D218 (LOGGED, parked) — wide FOV exposes the per-level
Visibility.FarFogdraw-distance/fog boundary ("blue artifacting" down the Dam tunnel at high FovScale).bgfog.c:305—FarFogis both far clip and fog end. Proposed fix (Video.DrawDistancemultiplier +Video.DrawDistanceAutoFovtoggle) written into the D218 finding; not implemented by user request.
Reconciliation state — DONE (M-84 cont.)
All three PRs merged to main: #36 (04a6372b), #37 (bdd924c7), #38
(7a7dfb94). Conflicts on merge were only findings.md /
findings-index.csv / QOL-INVENTORY.md row-adjacency + a trivial
video.c (portSkipIntro vs portFovScale coexist) — resolved keeping all.
findings-index.csv is generator-output (tools_pc/gen_findings_index.py,
enforced by the validate CI gate) — hand-edits fail CI; regenerate.
Throwaway test/m83-all + merged feature branches deleted.
NEXT SESSION
- Owed on
main: regenerate thetools_pc/goldenbunker1 capture — D215 makes the gun render in-frame, soverify.sh bunker1reports REGRESSION until re-baselined. (Interactive/console session required.) - D217 triage — viewmodel + 3rd-person Bond model textures garbled
(green/blue/yellow), watch face too. Stale-palette CI-texture bug, D161
family.
GE_TEXDUMPon a BUNKER1 weapon texture; checkgDPLoadTLUTreachesgfx_pc.cppfor model DLs. - D218 (parked) —
Video.DrawDistance+DrawDistanceAutoFov; spec in the D218 finding. Only if asked. - D211 nits owed: eyeball HUD-static + aim-zoom-composes at FovScale ~120.
build-pc/holds a stale build — rebuild.
M-85 (2026-09-09) — golden re-baseline done; D217 did NOT repro; D211 HUD-static confirmed.
Session env: interactive — build + verify.sh + GE_PCDUMP capture OK; no
scripted in-level input.
- Golden bunker1 re-baselined — branch
chore/golden-bunker1-rebaseline-d215(commit1e86bd66, offmain@7a7dfb94). Fresh default ini pinned 640x480,GE_PCDUMP=200-440:120. Gun eyeball-confirmed visible + posed + clean.verify.sh bunker1now PASS worst_cell 12.3 (was REGRESSION ~28). Needs push + PR + merge. - D217 — repro pinned to FACILITY. Clean on
-level_09BUNKER1 in every config;-level_34FACILITY reproduces at ~frame 300 (just after the vent-drop intro cutscene): PP7 grip = solid green block, green ring at the barrel joint, hand has a green cast, slide/barrel upper is correct black.GE_TEXIconfirmsgDPLoadTLUTDOES reachgfx_pc.cppfor model DLs (weapon tiles importfmt=2 palfmt=0x8000 palidx=0). User: it's widespread — guns on many levels, multiple weapons, wrong/ missing textures. Facility PP7 is just a clean headless repro.GE_D217probe (reverted): weapon model DLs issue partial-countG_LOADTLUT(count 1/28/125/172/193/…);GE_D217Z— zeroingrdp.palette[count..255]did NOT fix it, so the bad texels index within[0..count)⇒ wrong palette bound to the grip tile. Best theory:TextureCacheKeycollision / missing CI-cache invalidation on palette change (gfx_pc.cpp:990, no palette-content hash) — Facility renders green guards right before the viewmodel, grip takes a stalecache=HITagainst the guard-fatigue palette. Fix needs an all-21-levels world-CI regression sweep (hot path). Full next-steps in the D217 finding. Screenshots: scratchpadfac_gun_crop.png,fac_gun_D217Z.png. - D211 nits — headless FovScale 100-vs-120 diff: HUD ammo counter screen-position identical, no 4:3 stretch, viewmodel composes. HUD-static confirmed. aim-zoom-compose still owed (button-held, not scriptable here).
- D218 stays parked (untouched).
M-81 (2026-09-08) — D193 SOLVED (= D209). AI locomotion fixed + verified. PR #32 MERGED to main.
Branch diag/d193-timing-keystone-measured — merged to main 2026-09-08
(merge commit 34ae9f54, PR #32), branch deleted, local main fast-forwarded.
CI went green after regenerating docs/dev/findings-index.csv for the new D209
entry (chore(D209): regen findings index — the validate job's
gen_findings_index.py --check gate had caught it stale). No session trailers.
Root cause (D209, D3x pointer-width class). get_sound_at_range()
(chraction.c:3573 — misleading name; it is the locomotion-animation
selector) picks walking / running / sprinting from an arg1 speed tier. Its
only caller (chraction.c:3665) passed that tier as
self->act_ubytes.padding[45] — a raw byte alias into the ChrRecord action
union, not the named field. On N64 offset 45 is act_gopos.unk59. On
x86-64 the three pointer members of act_gopos widen 4→8 B, moving unk59
to 81, so the literal 45 lands on byte 5 of waypoints[1] — and because
the arena is low-4GB (0x00000000_70xxxxxx) that byte is always 0x00.
Result: arg1 = 0 forever → every AI chr in the game bound to
ANIM_DATA_walking / ANIM_DATA_walking_unarmed, on every level, since the
64-bit transition. GE travel is anim-root-motion driven, so they all moved at
walk pace no matter what the ailist commanded. Bond is unaffected (explicit
physics, no root motion) — exactly the reported "player fine / all AI slow" split.
Fix: chraction.c:3665, #ifdef PORT → self->act_gopos.unk59; N64 line
verbatim under #else. ABI-only, no logic change. Both callers reach it with
actiontype == ACT_GOPOS, so it is the same byte N64 reads.
User-verified on Cradle: Trevelyan and the guards run.
Also fixed the // guess: room comment on unk59 (bondtypes.h:2191) — it is
the SPEED tier.
Owed / next (also tracked in local gitignored docs/BACKLOG.md):
- Re-playtest the other D193 instances now that the fix is on
main: Facility Ourumov + squad beat, and Natalya escort pace on Bunker ii / Statue / Control / Archives. Expect all to resolve; confirm D170 (Ourumov/Trevelyan "flee" looks wrong) is subsumed. - Cradle completability — D193 was the reason the final level could not be finished. Needs a full end-to-end Cradle run to confirm 21/21 completable — this is the v0.2.0 gate.
- Probe scaffolding (
GE_D193/GE_D193A/GE_D193B) is env-gated and inert; strip once the re-playtest above passes. Commit/PR this branch— DONE (PR #32 merged).
Validation status: the fix is Cradle-verified only (Trevelyan + guards run). The other instances above and the full completion run are still owed; the PR body flags this explicitly so it is not read as fully validated.
Traps worth remembering (cost real time this session):
chrlvApplySpeed'sspeedPtrout-param is a turn rate, not a travel rate (chraction.c:8070passes&act_runpos.turnspeed).act_gopos.speed == 0means "running in a straight line", NOT "stalled".- The exe is
WIN32_EXECUTABLE(GUI subsystem), so PowerShell2>captures nothing. Use a.batwrapper (build-pc/d193a_cradle.bat) — cmd sets the handle before process start. Also needs the MinGW runtime dirC:\msys64\mingw64\binon PATH (libwinpthread-1 / libgcc_s_seh-1 / zlib1 / SDL2). PROP_TYPE_VIEWER = 6,PROP_TYPE_CHR = 3. The viewer stops going throughchrTickafter the intro, so Bond cannot be measured withGE_D193A.
M-82 (2026-09-08) — Lane C visual sweep + A1 sibling of D209. 4 PRs open.
Agentic session, focus = Lane C visual bugs toward 1.0. No display this session, so rendering fixes ship as "owed one screenshot" not "done".
Landed (all #ifdef PORT / port-only, verify.sh crash-free):
- D196 — PR #33 (ready). F10 options overlay left the OS cursor
visible on close unless you'd already clicked-to-lock in a stage. Fix:
re-assert
applyCursorVisibility()each poll afterreconcileGrab()(port/src/input.c). Owed: 5-sec eyeball (open F10 from a menu, close, cursor gone). - D210 — PR #35 (ready). New A1-class bug, sibling of D209, found by
the post-D209 sweep.
chrToPatrolinitsact_patrol.lastvisible60via the raw union aliasact_init.padding[0x13];act_patrolwidens (leadingpathptr) so on PC the write misses andlastvisible60stays garbage → the patrol "haven't seen the player recently" gate reads junk on entry to PATROL. Fix = named field under#ifdef PORT(chraction.c:3872). verify PASS bunker1/facility/archives/dam. Owed: patrol-guard behaviour eyeball. The rest of thepadding[N]sweep (anim/dead ticks → pointer-free arms) is layout-stable;struct player/handraw offsets (AUDIT-M6 #1-4) already fixed under D115. - D176(a) sky — PR #18 (draft, rebased onto main + Defect 1 fixed).
Cloud tile =
s_skywaterimages[0]64×64 IA8; thetc = S*32scale was right (matches GEwidth<<5idiom), the bug was(s16)overflow on the ±20k-texel horizon S/T. Fix = per-primitive phase-fold to the 64-texel period (sky.c skyPortRenderPoly). Defect 2 (horizon-straddling tris) still open → adaptive tessellation. Owed: one-level_22screenshot — seedocs/dev/D176a-SKY-NOTES.md"Verification ask".
Investigated, not fixed:
- D172 (magenta/cyan particles) — PR #34 (draft, diagnostics only).
Withdrew two dead root causes: 0xB9 is
G_SETOTHERMODE_L(handled), and the records do setG_CYC_2CYCLE(fast3d applies it — probe-confirmed). Real suspect: theG_CC_INTERFERENCEtwo-tile combine / TEXEL1 bind (IA8 smoke × RGBA16 fire @ tmem 0x188). Shipped a decisiveGE_D172=1probe — one Silo capture (close guard kill) dumps both texunits' tmem/fmt/siz and settles it.docs/dev/D172-PARTICLE-COLOR-RESEARCH.md§5-6.
Not worked (Lane D / audio): blocked on the user's by-ear playtest — D207 monitoring, D202/D204/D206/D208 fixed & need ear-check only.
Playtest validation (M-82, user, off merged main):
- D210 + D209 — PASS on Dam, Facility, Bunker. Guards react to the player and move at running pace. (Natalya-escort levels not yet checked.)
- D196 — not explicitly re-checked; user instead decided to backlog
removing
MouseCaptureMode=0entirely (it's the D192-bugged path). SeeBACKLOG.md"M-82 playtest feedback". - D176(a) sky — renders clouds but WRONG. Too small / over-repeating,
scroll too fast. Path B's ortho emit has no perspective-correct texturing;
unk20/unk24are pre-persp-divide S. Needs grid tessellation or Path A (RDPHALF decode). Detail inBACKLOG.md. PR #18 merged; revert candidate if it reads worse in motion than black. - Audio — SFX good; title/intro/menu music "mostly accurate". Not a full N64 A/B but positive.
- D172 probe — not run yet.
M-80 (2026-09-08) — B1/D193: timing keystone MEASURED, hypothesis contradicted (superseded by M-81)
Branch diag/d193-timing-keystone-measured (commit b457c014, not pushed).
Added GE_D193=1 probe in waitForNextFrame() (per-wall-second: render fps /
sim ticks Σ / clamp hits / max raw delta). Idle runs of -level_09/_20/_27/_34:
dead-steady render=30 fps, sim=60 ticks/s, clamped=0, maxraw=2 on every
level, VSync irrelevant. The wall-clock sim sum is exactly N64's ~60 — B1's
"g_GlobalTimerDelta / g_ClockTimer sum low" premise is wrong for steady
state. Findings D193 + GE-ENV-PROBES.md updated.
LIVE-LOAD CONFIRMED (M-80 cont.): user ran Cradle -level_41 with GE_D193=1
and drove the Trevelyan sequence while Trevelyan was visibly walking really slow —
probe held render=30 sim=60 clamped=0 maxraw=2 dead steady, no [D156] fires.
D193 is a locomotion/animation bug, not the frame clock. The pivot is final.
D193 port side now instrumented (M-80 cont., commit 43d20538): GE_D193A
(chr.c chrTick — per-sec travel speed + root-motion scalars) and GE_D193B
(model.c sub_GAME_7F06D3F4 — raw anim-bitstream decode). Silo+Facility:
every port-side input reads nominal — g_GlobalTimerDelta=2.0,
playspeed=1.0, scale=0.1, anim_translation_scale=1.0; the C in this path is
identical to N64 (only non-firing D156 guards are #ifdef PORT). animrate=0.0
is the default. Patrol guards travel ~60–99 u/s. GE_D193B: base=0/angle=0
every call.
Next for D193 — BLOCKED on a reference oracle. M-80 audited the anim path
end to end: frame clock (ruled out, measured), all model/chr scalars (nominal),
ModelSkeleton (ruled out — static C), anim-data endianness / D33 fixup (audited,
complete for this path — walk/run records are in animation_table_ptrs1, no
double-swap, no fixup errors, sizeof matches both platforms). Anim decode is
structurally sound on static review — a magnitude error, if any, needs the N64
side to see. Two paths (fresh session — no MIPS toolchain / emulator here):
(A) set up mips-linux-gnu + IDO-recomp + an N64 emulator w/ osSyncPrintf capture,
build ROM with #ifndef PORT logging matching GE_D193B, diff same scene;
(B) user records N64 footage of one guard over a measured distance, frame-count
→ port-vs-N64 speed ratio (clean 2x = per-frame/per-tick doubling; fractional =
scale/data error). Recommend (B) first.
Residual (unobserved) frame-clock risk: no remainder carry in the per-frame round — a steady 45–55 fps scene would under-advance; only bites in the 20–30 fps band, which the port never enters. Parked.
Milestone (2026-09-04, M-50) — v0.1.0 SHIPPED
v0.1.0 alpha is published (GitHub release, tag v0.1.0, marked
pre-release by the user's choice — no green "Latest" badge is expected).
Both bundles + .sha256 attached, smoke-tested. Cut off the M-49 state:
full-campaign playtest, 18/21 missions completable — Bunker ii + Statue
crash (D191); Cradle can't be finished (D193 scripted-sequence stall, not a
crash). PR #25 (docs reframe + hygiene) merged.
Open / owed after the release:
- PR #26 — M-49/M-50 doc-accuracy corrections (18/21, Cradle=D193,
D149 flagged stale, CI
<version>substitution fix). Needs merge. The published release body still shows19 of 21/ literal<version>— edit it to match (corrected full text was prepped in this session's scratchpad). - social-preview.png — regenerate from
.github/social-preview.html(updated) + re-upload via Settings → Social preview. Script in.github/social-preview.md. - README hero screenshots — user has
boris/archivesshots (indocs/img/screenshots/, gitignored); need resize/crop + move into the trackeddocs/img/, then wire into the README hero row + release body.
Now targeting 1.0. Plan of record: docs/dev/notes/ROADMAP-1.0.md
(local-only; dependency order: B1 timing keystone → B2 D191 crash →
B3 audio ∥ → B4 GE_DETERM → B5 cutscenes → B6 visual wave →
B7 input). Next dev milestone M-50/51:
docs/dev/notes/AGENTIC-SPEEDUPS-PLAN.md Steps 1/2/4/5 (verify.sh,
context-tax, crash→brief) + get the D191 gdb backtrace and fix it → 21/21
crash-free → v0.2.0. Then B1 (instrument
g_GlobalTimerDelta/g_ClockTimer, root-cause D193).
Local working notes live in docs/dev/notes/ (gitignored) alongside this
file and docs/BACKLOG.md.
Direction
Post-v0.1.0: driving to a playable 1.0. See the Milestone block above and
docs/dev/notes/ROADMAP-1.0.md. The earlier "breadth-first, sweep all 21
levels for load+render+no-crash" pivot (M-11 → M-30) is done — all 21
load and render, 18/21 completable end to end.
Diagnostic env probes
All GE_* env-var probes in the tree are catalogued in
docs/GE-ENV-PROBES.md (var → file:line → what it does → live/dead).
Live tooling: GE_PCDUMP, GE_INPUTSCRIPT, GE_STARTMENU, GE_UNLOCK_ALL,
GE_INPUTLOG, GE_SAVELOG, GE_D160, GE_DTEX, GE_TEXDUMP. The rest are
closed-finding GE_Dxx scaffolding (strip candidates; all env-gated and inert,
except bg.c's GE_D63 lines which are not #ifdef PORT-guarded).
Playtest bug reports (M-49 cont., 2026-09-04) — logged for future triage, NOT v0.1.0 blockers
User playtested the v0.1.0 win64 bundle (<games>\goldeneye-pc-port-0.1.0-win64,
fresh save). Bugs filed in findings §F (D191–D197), all alpha-acceptable.
NB: Cradle does not crash — it's the D193 scripted-sequence stall (an
earlier note here wrongly said "Cradle also crashes").
-
D191 — SIGSEGV in
modelGetNodeRwData(model.c:478), thewhile (root->Parent)node-tree walk. TWO instances, identical fault PC0x1400801b1: (1) killed the first guard after escaping the cell in Bunker ii; (2) Statue, right after the Trevelyan-meeting cutscene. Instance 1Rdx = 0x70267bc0<<32(32-bit ptr in the high half); instance 2Rdx = 0, FAULT ADDR0x0(NULL). BothR8 = 0x1d. A model'sParentchain has a truncated/NULL link → pointer-width / byte-swap family (D119/D122/D3x), NOT weapon-fire-specific. Crash logs saved toscratchpad/crashlogs/. Shipped exe has full symbols;debug-crash.ps1in the bundle dir runs it under gdb +GE_D51=1(→d52rw.log). **Awaiting a realbtp *root/p *Objinst** — the crash.c EBP walk broke (#01: 0x1d). No fix.
-
D192 — mission-select grid pointer can't reach outer cells,
MouseCaptureMode=0only. D169's "FIXED" clamp routes throughgetPlayer_c_screenwidth()=g_CurrentPlayer->c_screenwidth, which is the ~320×240 stage viewport in the front end, not the real 440×330 (front.c:8570). Relative P-controller path (input.c:659-690) winds up short of the grid's 317/235.5 outer split points. DefaultMouseCaptureMode=1(absolute cursor,input.c:629-657) feels fine. Fix is port-only + small: use the real front-end rect ([20,420]×[20,310]) in themenuModebranch. Confirm withGE_INPUTLOG=1in mission-select. -
D193 — NPC AI locomotion too slow. ⇦ user's "biggest overall flaw" after the full playthrough. NOT mainly an anim bug — scripted/AI characters (guards, Trevelyan, Ourumov, Natalya) travel to their destinations slower than N64. Leading hypothesis: the D117/D134/D155/D156 wall-clock timing family —
g_GlobalTimerDelta/g_ClockTimer(which scale everypos += speed*dtandwhile(i<g_ClockTimer)sim loop inchr.c/chrai.c) sum low over a real second, so delta-scaled AI motion lags while input-driven player movement feels fine. Ties to D170. Next: logg_GlobalTimerDelta+g_ClockTimerper frame, check the 1 s sum vs ~60; time a guard's run vs reference footage. -
D194 — mouse input needs another pass. (a) RMB aim is near bang-bang: the
input.c:697-70761 + gain·Δclamped to[61,80]gives a tiny proportional band that saturates almost immediately → "way too sensitive." (b) Sensitivity couples to frame/sim rate — raw stick counts emitted per poll, nog_GlobalTimerDelta/ polls-per-frame normalization, so it shifts with fps (reads as "correlated with movement speed"). Needs a real response curve + dt normalization. Config knobs exist; the mapping shape is the problem. Lineage D118a/D165/D166/D180-B3. -
D195 — transparency / alpha surfaces broken on Control (Natalya's console room — glass partitions + translucent projected wall displays). Failure mode not yet specified by the reporter. fast3d render-mode / alpha path; neighbours D161/D172/D176(b) but a blend defect not a decode one; check D128 portal adjacency. Logged in
GRAPHICS-BACKLOG.md— needs aGE_PCDUMPcapture.
Testing infra — FOR REVIEW: docs/BACKLOG.md → "Agentic dev + test speed-ups"
now has a testing-tier re-prioritisation (post-M-49): Tier 1 (GE_PCDUMP+framediff)
caught none of D191–D197; build a Tier 2 GE_INPUTSCRIPT scripted-playthrough
harness (+ crash-log check) before GE_DETERM, and fix the crash.c EBP
backtrace walker (both D191 dumps died at #01: 0x1d). Reconcile against the
10-step AGENTIC-SPEEDUPS-PLAN.md.
Playtest progress (M-49) — full-campaign playtest on the v0.1.0 win64 bundle. User played every solo level on the packaged build. Two levels crash mid-mission — Bunker ii and Statue (shared fault PC / root cause = D191). Cradle can't be finished — it does not crash, but Trevelyan's final-level scripted sequence stalls (D193). So 18/21 are playable start to finish — retires the "expect crashes once past the level intro" framing. Remaining playtest notes, in priority order:
- D193 — NPC AI moves to destinations too slowly. User's single biggest
complaint. Cradle is "bugged out completely" from Trevelyan's behaviour —
the final level's scripted progression breaks, so this is effectively a
can't-properly-finish-the-game bug, not just cosmetic. Top post-alpha fix.
Subsumes D170. Hypothesis:
g_GlobalTimerDelta/g_ClockTimersum low → delta-scaled AI motion + path-following lags. Instances: Facility soldier squad walks in slowly + takes ages to reach firing positions (regular combat AI — not a named script); Silo/Cradle named-NPC flee; Natalya (escort) always walks really slow on every escort level. - D191 — the two-level crash (Bunker ii / Statue). Needs a gdb
bt. - D173 — third-person Bond model hovers above the ground at level load. Confirmed still present across the whole playthrough. GRAPHICS-BACKLOG.
- D148/D160 — cutscenes buggy in many places (not just the Dam ending): skipped, wrong camera, misplaced/hovering actors, timing. Now the tracking entry for cutscene reliability generally. Likely systemic — propDef stride desync (D122/D132) + D75/D173 actor bugs + D155/D156/D193 timing.
- Minor: D192 (menu grid pointer, always-grab only), D194 (mouse aim curve + dt),
D195 (Control transparency), D196 (OS cursor stays visible after closing the
F10 overlay in click-to-lock — 1-line fix identified:
applyCursorVisibility()afterreconcileGrabatinput.c:522), D197 (character face/head textures wrap around the head —G_TX_CLAMPnot honoured / non-PoT wrap period, seen on Silo), plus the older graphics backlog (D176 sky, etc.).
Done this session (M-79) — D191 (Bunker ii / Statue mid-mission crash) ROOT-CAUSED + FIXED. Branch fix/d191-bondview-cuff-switch-stride.
D191 FIXED (src/game/bondview2.c bondviewSelectCuff, #ifdef PORT;
one-branch ABI fix). User ran the shipped bundle under gdb (new
debug-crash.ps1 written into the bundle dir — the old one was missing) and
reproduced the Bunker ii first-guard-kill crash with a full backtrace.
The M-49 findings guess was wrong about the site. The fault is NOT the
root->Parent node-tree walk — root arrives at modelGetNodeRwData
already garbage (0x70267a9000000000 = the u32 0x70267a90 with its
halves transposed), passed straight in by frame #1
bondviewSelectCuff(model, header, switchindex=29) <- gunUpdateAndFire(GUNRIGHT)
(runs on the frame the player fires). bondviewSelectCuff byte-indexes
header->Switches (a ModelNode * array — 8-byte stride on PC, widened per
D43/D45) with offset = switchindex << 2 (N64 4-byte stride); every base[N]
deref then lands mid-slot. Identical to D141 (gunfire.c had the same
Switches byte-index bug, fixed the same way; this call site was missed).
Fix: PORT-only offset = switchindex * (s32) sizeof(ModelNode *); the
downstream base = (u8*)switches + offset math and the #else N64 line are
untouched. The Statue instance (Rdx=0, NULL) is the same site landing on a
zero-reading slot.
Verified: build clean; -level_09 (bunker1) golden framediff 3/3;
-level_27 (bunker2) + a scripted-fire GE_INPUTSCRIPT run crash-free.
PLAYTEST-VERIFIED (M-79): user replayed Bunker ii (first-guard kill) and
Statue (post-Trevelyan cutscene) on the fix build — neither crashes. That's
21/21 solo levels crash-free → v0.2.0 gate met (Cradle's remaining problem
is D193 timing, not a crash). Fix build staged in the bundle dir
(ge007.x86_64.exe.D191FIX / playtest-D191.bat; restore-v0.1.0.bat reverts).
(A headless scripted-fire repro of the pre-fix crash was inconclusive — the
quick -level_27 boot doesn't set up the fresh-save Bond model state the
crash needs — so the human replay is the real verification, same as D206/D208.)
Findings §F D191 updated; porting-notes.md §B (D141 entry) extended; ROADMAP B2 is now done pending playtest.
Done this session (M-78) — feat/phase3-audio MERGED to main (PR #28); D205 CLOSED; D207 probe re-armed for a real-level repro.
PR #28 merged (16-commit Phase 3 audio branch: software mixer D198–D206, D202 closed, D204 tempo, D206 bank-index). CI green, fast-forward, branch deleted.
D205 CLOSED (M-78). User re-tested the explosion→scream symptom on Bunker post-merge: the explosion SFX is correct by ear now. All four D205 symptoms were D206 (the +1 bank-slot alias); the earlier "scream" was a coincident guard death-yelp (faithful). No presentation-layer bug — M-74's pan audit already showed GE never spatially pans SFX.
D208 FOUND + FIXED (M-78c) — player fire sound goes permanently silent
mid-firefight. This is the "player gunshots stop and stay muted after the
alarm ends" half of the user's D207 Bunker repro — a distinct bug from
D207's voice-pool question. bondview.h:200 s32 field_A48 is used as a
second ALSoundState * handle slot for the player's double-buffered per-hand
fire sound (gunfire.c:3186-3200); sndPlaySfx stores an 8-byte pointer
through &field_A48 (pendingState->link.next = nextState) which, at s32
width, tears into field_A4C (the per-frame auto-fire cadence timer). After
enough double-buffered cycles field_A48 is a non-0 non-pointer → the
else if (field_A48 == 0) re-arm gate never fires again → no further player
gunshot. Proven in the user's audiotrace.log: silenced-PP7 idx-46 requests
stop at line 18930/25086 while everything else continues; slot 0x70076630
caught holding torn 0x000001db00000000. Fix: #ifdef PORT → field_A48 is
ALSoundState * (runtime-only struct, layout-safe). Build clean.
Runtime verification owed: user replays the Bunker firefight, idx-46
sndPlaySfx should continue the whole time.
D207 — still needs a real-level repro; probe re-armed.
The user's Bunker capture showed the voice pool never above 7/8 and the SFX
event queue peaking ~12/64 — so D207's "(a) heavy-level 8/8 starvation" is
still unproven. Stays MONITORING pending a Facility/Silo/Statue capture. Re-added just the
[D207-DROP] counter (NOT the reverted last-resort preemption pass) at both
sndDisposeSound no-voice sites in src/snd.c, gated on GE_AUDIOTRACE,
logging state/prio/flags/count=N/max. Static re-check of ALARM3
slot 162: sampleVolume=80 (quieter than gunfire's 90–110 — weakens the
"mixed too loud" theory); keyMax=8 → NOT RETRIGGER → continuous looped by
design on N64 too. build-pc/d207_run.ps1 = forced-alarm auto-repro on
level_09. Blocked on the user: which level + weapon/situation they hit
"alarm overrides everything / doesn't recover", and a GE_AUDIOTRACE=1
capture there. Then: [D207-DROP]>0 + count pegged at 8 → (a) heavy-level
starvation → re-apply the reverted last-resort pass; alarm voice never freed
after alarm-off → (b) leak on a stop path; drops=0 but still wrong → (c)
perceptual, compare drone loudness/loop against N64.
Still to do after D207 closes: prune the D202 probe set — GE_AUDIOTRACE
(src/snd.c ×many, src/game/propobj.c, src/libultra/audio/*,
src/game/mp_music.c), [VOL], [DISTVOL], [D207-DROP], GE_FORCEALARM,
#include "audiotrace.h". Keep the D206 #ifndef PORT/#else -1.
Done previous session (M-77) — D206 validated by playtest; D207 investigated (fix tried + reverted). — then prepped feat/phase3-audio for merge (M-77c).
MERGE PREP (M-77c): user by-ear A/B vs N64 confirmed D206 (armour / melee /
silenced PP7 all correct) → D202 CLOSED, D205 narrowed to symptom (2),
D207 → MONITORING (acceptable in play). Commits 378386d1 (D206 fix),
04a539ba (GE_FORCEALARM probe), c25285d8 (doc status), d56c15d9
(GE-ENV-PROBES row — fixes the validate CI job). All pushed.
PR #28 retitled + body rewritten to cover the full Phase-3 audio scope
(D198–D206, D202 closed); validate now green; waiting on Win/Linux build
checks. mergeStateStatus BLOCKED only = CI in progress; no branch protection,
no required review → user can merge once green. Local verify: -level_09
framediff 3/3 + crash-free, -level_20 crash-free.
After merge: delete feat/phase3-audio; next audio work = D205 (explosion
→scream) and, if it recurs, D207 on a heavy level. Prune the removable D202
probe set (GE_AUDIOTRACE/[VOL]/[DISTVOL]/GE_VOICEDUMP/…; keep [EXPIRE]
and GE_D204) + GE_FORCEALARM once D205/D207 close.
Original M-77 notes
D206 (every SFX one bank slot too high) — FIXED and PLAYTEST-CONFIRMED.
struct ALInstrumentAlt_s.soundArray (src/snd.h:165) is at struct offset
12 on N64 (3×s32, 4-byte ptrs) but 16 on PC (8-byte ptr + alignment).
N64's offset-12 array aliases the on-disk bendRange/soundCount words so
soundArray[N] == on-disk entry [N-1] — GE's SFX_IDs are 1-based by
design. Fix: src/snd.c:1001, #ifndef PORT/#else → `soundArray[soundIndex
- 1]
, N64 line verbatim (D3x ABI/layout class). Build clean; BUNKER1GE_AUDIOTRACE60s crash-free, all ~90 distinct indices resolve torom_sfx_decode.pyslot N-1;-level_09` golden framediff 3/3 ×2. User playtested: "most audio is good now." Root-causes D202's silenced-PPK "slap" and D205 (3)/(4). NOT D205 (2).
D207 (alarm klaxon "overrides everything, doesn't recover") — INVESTIGATED,
NOT SOLVED. D206 correctly turned ALARM3_SFX from a wrong one-shot into the
real infinite-loop klaxon (slot 162). Hypothesis was: it permanently holds 1 of
8 SFX voices → combat SFX starve. Built the designed fix (last-resort
LOOPED-voice preemption in the snd.c scan) + instrumented the real drop
sites. Result: it doesn't hold up. BUNKER1 GE_FORCEALARM + scripted-fire
soak: 0 true drops, the new pass never fired, voice count recovered to 0
after every alarm-off (no leak). The "~20% drop" from the earlier session was a
log-latency measurement artifact. The fix was reverted — snd.c carries
only the D206 -1 now. BUNKER1 combat is too light to peg the 8-voice pool, so
either it starves only on a heavy level, or the symptom is a different
mechanism (leading guess: perceptual — the klaxon is a gapless continuous
loop where a real GE alarm pulses, and/or slot-162 is mixed louder than N64).
Full write-up: findings.md §F D207 + detail (M-77b section).
Temp probe left in tree (intentionally, for the D207 follow-up):
GE_FORCEALARM in handle_alarm_gas_timer_calldamage (src/game/propobj.c,
#ifdef PORT / getenv, inert unless the env var is set).
Uncommitted: src/snd.c (D206 -1 fix only), src/game/propobj.c
(GE_FORCEALARM probe), docs/dev/findings.md (D205 M-73/M-74 + D206 + D207),
docs/porting-notes.md (§E), docs/HANDOFF.md. Nothing committed.
Next session, in order:
- D206 sign-off — user by-ear A/B vs N64 (armour pickup, unarmed melee whiff, silenced PP7). If clean: D202 closes outright, D205 shrinks to symptom (2) alone.
- Commit D206 + the pending D205/M-73/M-74 doc changes (independent of D207, and playtest-confirmed).
- D207 needs a real repro from the user before any code — ask which
level + weapon/situation, and get a
GE_AUDIOTRACE=1capture (or clip) of the failing alarm. Then decide between: (a) genuine 8/8 starvation on a heavy level → re-apply the reverted last-resort pass (design is infindings.mdD207 detail); (b) a leak on a specific alarm-stop path; (c) perceptual — compare slot-162 playback volume + whether it should pulse (SOUND_FLAG_RETRIGGER) against N64. Soak Facility/Silo/Statue withGE_FORCEALARM+ sustained fire watching[D207-DROP]/count=8if no user capture. - Remove temp probes once D207 is resolved:
GE_FORCEALARM(src/game/propobj.c); the D202 set —GE_AUDIOTRACE(src/snd.c×several),[VOL](src/snd.c:556),[DISTVOL](src/game/propobj.c:12780) +#include "audiotrace.h". Keep the D206#ifndef PORT/#else-1. docs/dev/notes/D206-IMPL-BRIEF.mdcan be deleted (superseded).
Prior session (M-74b/M-75) — D206 found + reframed. (Fix-direction claims below are SUPERSEDED by M-76 above; the evidence still stands.)
The user supplied a verified sound rip (~/Downloads/Nintendo 64 - GoldenEye 007 - Miscellaneous - Sounds,
B00I00S<hex>.wav, 186 files — filenames are HEX bank indices) and
identified three sounds by ear. That broke the case open.
Proven: rip index == bank index (186/186 exact PCM frame-count matches vs
rom_sfx_decode.py; 0/185 at ±1). Correct armour = S50 = bank 80, what the
port plays = S51 = bank 81 — and the game requests 81. The actual Klobb
sound = S69 = bank 105 — the exact index the game requests for a punch
whiff (PUNCHING_AIR_SFX). Both off by +1.
Structural confirmation: ROM bank soundCount = 261 (valid 0..260), but
SFX_ID has 262 members starting with the NOTHING_SFX = 0 sentinel that
snd.c:992 short-circuits. 261 real IDs (1..261) fit 261 slots (0..260) only
with a -1. And BIG_CLANK_SFX = 261 currently reads one past the end.
src/snd.c:1001 is unmodified decomp: soundArray[soundIndex].
D202 corroboration: scratchpad/B00I00S2D.wav — the reference clip that
drove D202 from M-60 to M-67, provenance "found online" — is a file from this
same rip. S2D = bank 45, and wppksil_stats.Sound = 0x2E = 46.
M-61's rejected "index 45 matches at +0.999" was right all along, and the
"slap" on every silenced PPK shot is bank[46] = PUNCH1_SFX.
Why eight sessions missed it — read porting-notes.md §E: the verification
tooling shares the bug. exact_match.py compares a runtime voice to a ROM
decode of the requested index with the same missing -1; diff_bank.py
walks both layouts with identical indexing. M-68/M-70 proved self-consistency,
never correctness.
Explains: D202 silenced-PPK slap ✔ · D205 (4) melee→Klobb ✔ user-confirmed · D205 (3) armour ✔ user-confirmed · D205 (1) slap/glass over gunfire (consistent, unconfirmed). NOT explained: D205 (2) explosion→scream — 169-183 maps to 168-182, all still explosions; stays with D205, M-73's blast-killed-guards reading is the likely answer.
Next session, in order (M-75 — supersedes M-74b's list):
- Work the
docs/dev/notes/D206-IMPL-BRIEF.mddecision gate, candidates A→C→B (cheapest first). (A) is the rip keyed bySFX_IDnot by 0-based bank slot? — re-auditrom_sfx_decode.py'ssound_offsetswalk vs the runtimesoundArray; if yes there is no engine bug. (C) doesGE_AUDIOTRACElogsoundIndex=81(not 82) for an armour pickup? (B) byte-compare PC runtimesoundArray[81](GE_AUDIOTRACEprobe atsnd.c:1002) torom_sfx_decode.pybank 80 vs 81 → if PC index 81 == ROM bank 80, converter (port/src/romdata.c) dropped an entry. - Only once a locus is proven with a byte-level artifact, apply the fix there. If it lands on (D) genuine data-side (+1 in ROM constants), stop and escalate to the user — not a legal port edit.
- Re-verify. Fix
exact_match.py/diff_bank.pyto the agreed convention first (they pass either way otherwise). Re-run the M-72 controlled captures: silenced-PPK →bank[45](ripS2D), punch whiff →bank[104], armour →bank[80](S50). - User by-ear pass on melee / armour / silenced PPK vs N64.
- Then the D202 probe removal + close listed below.
Done this session (M-74) — D205: M-73's root cause WITHDRAWN. Its closing conclusion ("the PC side is exhausted; the N64 A/B is gating") is SUPERSEDED by M-74b/D206 above — the bug was in the index mapping all along. The M-73 withdrawal and the pan audit below both still stand. Re-counting the same capture shows the "idx 109 barrage" is normal in-spec guard combat; the one untested layer left is spatial presentation (pan/volume). Next session starts here.
M-74 (findings.md D205 + §F row + porting-notes §E). M-73 concluded that
guards were stuck re-triggering fire sound idx 109 ("256x over 29.7 s, no
bullets") and pinned it on stanTestLineUnobstructed guard→Bond LOS over
converted collision geometry, then queued a full pccg-stan decode + ray-replay
investigation. Re-counting the same file (build-pc/audiotrace.log) breaks
every load-bearing number:
- idx 109 is requested 128×, not 256 — the
GE_AUDIOTRACEdouble-log artifact D204 already corrected for M-63 on this exact sound index. CountsndPlaySfx: t=lines, never baresoundIndex=matches. - 128 / 29.73 s = 4.31/s aggregate across ~11 guards ≈ 0.4/s each —
about 6× UNDER the AK47's own
SoundTriggerRate(RATE_AK47= 4g_GlobalTimerticks ⇒ 15/s per guard ceiling). Thefield_178gate that M-73 accused never binds. - "No bullets/impacts" is false: the same window holds ~30 ricochet/wall-hit requests (19–41), 69 ×2 flesh, 12 body-falls (123–132), 14 guard yelps (134–147). Guards were firing and hitting.
- The striking monotone sweeps 134→147 and 123→132 are the ground-truth
round-robin
male_guard_yelp_counter(chraction.c:2454-2470) — faithful cycling in byte-matched code, not a brokenrandom() % n.
Do NOT open the pccg-stan / LOS geometry investigation on this evidence. The only open observation left from that capture is that the firefight lasted a while, which is a gameplay-fidelity question for an N64 A/B.
Where D205 stands. Everything upstream of presentation is proven: bank
conversion 261/261 identical (M-71), sample+pitch exact (M-68/M-70), and every
requested index in the capture correct (182 explosion ×1, 81 armor ×1,
46/47/48/49/105). That leaves one untested layer: spatial presentation —
per-voice volume, pan, concurrency. It explains all four complaints in one
shape (a wrongly-placed sound is, by ear, a sound at your own position: "a gun
sound came out of my punch", "the explosion screamed", "a slap over my
gunshot"), and it is the youngest code in the stack — sndCreatePostEvent was
stubbed out entirely by D138 until M-65 un-stubbed it, and has never been A/B'd.
Ranked: H-A pan/spatialisation wrong or absent in the mix (top) · H-B
distance law right but scale wrong ([DISTVOL] follows the documented curve
yet only reaches 0.60 of max at dist 1794; never compared to N64) · H-C
voice-pool masking (weak — M-70 measured alloc ≤ 5/8) · H-D PC guards
genuinely engage where N64 guards don't (possible, but now with no
supporting evidence; gated behind the N64 A/B).
Previous session (M-73) — D205: proposed a guard LOS/collision root cause. WITHDRAWN by M-74 above — the counts it rests on do not survive re-derivation. Kept for history; do not act on its next-steps.
M-73 (findings.md D205 + §H): analysed the user's full-combat
GE_AUDIOTRACE=1 capture (build-pc/audiotrace.log, 288 unique requests).
idx 109 (GUN_B4_BOLTACTION_SFX, AK47/Spectre fire sound) is requested
256× over 29.7 s, from ~11 distinct NPC state slots, each on its own fixed
period (median gaps 200/434/499/698/934/1299 ms — multiples of ~200 ms).
Only ~32 non-109 requests exist in the window (~0.6/s) → guards re-trigger
weapon fire sounds WITHOUT firing bullets (no impact SFX at 10×/s). Code:
chrlvFireWeaponRelated (chraction.c:6530) passes phi_a2 = sp27C || sp278 to sub_GAME_7F02BFE4 (:6902) — non-bullet auto ticks (sp278) still
play the weapon sound, gated only by SoundTriggerRate/field_178; so
sustained ACT_ATTACK = sustained roar. Sustained attack needs
seen_bond_time >= g_GlobalTimer - 100 (:6593), refreshed by
setSeenBondTimeToNow behind stanTestLineUnobstructed(...OBJS|DOORS|CHRS| PATHBLOCKER|AIOPAQUE...) LOS queries (:3917/:3972, chrCanSeeBond :3945).
Leading suspect: converted collision geometry makes guard→Bond LOS return
clear where N64 blocks → guards never lose their target. Per symptom:
(1) "Klobb on melee" — 106 was NOT requested anywhere in this capture, and the user WAS punching in it (47/48/49 ×13, 105 ×19, all voiced); the only gun sound present during those punches was the 109 barrage. OPEN: is the heard "Klobb" a 109, or a real 106 from another session? (Klobb = specific SMG = Skorpion, idx 106 — user's N64 A/B identification stands; do not assert misidentification.) Decisive: ROM samples 106 vs 109 as WAVs for the user + targeted punch capture.
(2) "explosion→scream" — 182 requested once (correct); GET_HIT_MALE
(137–139) ±1 s around it, all under the barrage. (3) "armor wrong" — 81
requested once (correct!), bracketed by 109 ticks. Same phenomenon as D202
M-63's unexplained idx-109×94/60 s anomaly.
Done this session (M-72) — D205: fourth symptom added (unarmed melee → Klobb shot idx 106); controlled GE_AUDIOTRACE runs exonerate every request path; mechanism (b) extra-events now the leading suspect.
M-72 (findings.md D205 + §H): environment fix first — the "flaky /GS
crash" was SDL2.dll missing from PATH (C:\msys64\mingw64\bin; exe is
-mwindows, so a DLL-load failure just dies 0xC0000105, no console). Launch
via build-pc/d205_*.ps1 (gitignored) which prepends it. Controlled traces:
(1) PPK fire at -level_09: per shot exactly {46 GUN_SILPPK, one of the
20-entry ricochet_sounds_small table (gun.c:415 — rnd1 % 20 is IN RANGE),
122 CART_SPENT} — no glass/slap layer. (2) Levels 01–08 all start unarmed;
fire-press requests ONLY 105 PUNCHING_AIR (whiff). (3) Static: punch hit =
constant {47,48,49} (gunfire.c:2338); idx 106 is data-only
(skorpion_stats.Sound = 0x6A via bondwalkItemGetSound); the player
fire-sound path (gunfire.c:3192–3200) needs GUN_ANIM_STATE_FIRE, which a
fist never enters — so Bond's own punch CANNOT request 106; an NPC guard
firing its Skorpion (chraction.c:5588) can. No symptom reproduces in
micro-scenarios → the layering must come from extra/different EVENTS during
real gameplay (mechanism b).
Previous session (M-70 + M-71) — D202 data-closed AND by-ear-closed: probe removal + close is all that remains. NEW ACTIVE TASK: D205 — PC requests wrong/extra SFX indices vs N64 (explosion→scream, armor pickup wrong sample, slap/glass over gunfire); bank converter + playback chain exonerated.
M-70 (findings.md): decisive clean re-capture, 209 scripted -level_09
requests → every voiced request plays the exact ROM sample at the exact ROM
pitch (200/209 auto CC ≥ 0.85; the 4 MID are idx=109 verified 0.946 by
manual ratio sweep; the 5 NO-WIRE = 1 matcher window artifact + 4
never-voiced requests, NOT pool exhaustion — alloc ≤ 5/8 at each drop).
Zero wrong-sample playback. Matcher v3 fixes in scratchpad/exact_match.py:
death-clip both sides, slide() sparse-tail bug (step-1 over 0..2205,
step-4 across the span, ±8 refine), PLAY window 600 ms → 2 s. Output:
scratchpad/exact_run6_match.out. Trace files in build-pc/
(audiotrace.log, audiotrace_wire.log, voicedump.raw).
M-71 (findings.md): user by-ear pass on M-66b = PASS ("the metal
door sound does not loop forever now") — Disposition C validated; D202's
only remaining step is probe removal + close. The user then reported a NEW
complaint class, N64-A/B confirmed non-faithful: (1) slap/glass layered over
general gunfire (audible on PC, silent on N64), (2) explosion plays a
soldier-scream sample, (3) armor pickup wrong sample. Split out as D205.
This session exonerated the two remaining static suspects: the D37 bank
converter offline (scratchpad/banktest/: real romdataFixupAudioBank
harness + diff_bank.py → 261/261 entries identical N64 vs PC, 0
mismatches) and the D154/D135 GBI raycast parsers + volume law (static
re-audit, ABI-correct). So D205 = PC requests different or extra sound
indices than N64 — converted level/setup data (sndID / explosion-type /
item-sound fields in pccg sidecars) or extra hit events from converted
collision geometry. Full ranked next-steps in findings.md D205.
Next session, in order (M-74 updated — M-73's list is withdrawn):
D205 step 1, static audit of the presentation layer— DONE this session. H-A FALSIFIED. The port's stereo path is correct end to end (snd.c:461/530→alSynSetPan→env.c:400-402eqpower L/R targets →port/src/mixer.caEnvMixerImplindependentdry[0]/dry[1]gains), but GE never uses it:AL_SNDP_PAN_EVThas no poster anywhere in the tree, and 252 of 261 bank sounds havesamplePan == 64(AL_PAN_CENTER) — every index in the user's capture included. GE does not spatially pan SFX on either platform. Bankpanvolre-diffed 261/261 clean (closing the one field-coverage gap in M-71's prose). Volume formula has no PC-side overflow divergence.D205 step 2, pan probe run— cancelled: pan cannot vary, by design.- The PC side of D205 is exhausted. Bank, playback, requested indices, sample+pitch, pan and per-sound volume are all verified faithful. GE's only spatial cue is distance volume, and it is shallow by design (0.60 of max still at dist 1794) — so a guard firing across the room plays near-full volume, dead centre, at a per-sample volume comparable to the player's own punches (109 = 90; 47/48 = 100; 49/105 = 110). That is faithful and it is exactly the condition that produces all four complaints. Only H-D remains — does the PC generate MORE overlapping combat events than the N64 — and nothing on the PC alone can settle it.
- ⇒ The N64 A/B is now the gating step (user-side, owed). Batch it:
(a) ROM samples 106 (Skorpion / "Klobb") vs 109 (AK47) as WAVs via
scratchpad/rom_sfx_decode.pyfor the user to settle the Klobb identification by ear — their identification stands either way, this decides which sound, not whether they heard it; (b) N64 A/B: punch a guard while others fire — does the same combat sustain, and does the same layering happen, on N64? If N64 sounds the same, D205's four symptoms are faithful and the finding closes as NOT-A-BUG. If it does not, the difference is gameplay-side (guard engagement), and that is when the collision/LOS geometry work becomes justified. - Close D202: remove the probe set —
[VOL](src/snd.c:556),[DISTVOL](src/game/propobj.c:12780) + its#include "audiotrace.h", and the rest listed in GE-ENV-PROBES.md ([WIRE],[EVT]/[STOP-EVT],[VOICE±],[SLOTWRITE],[ENVELOPE],[VOICES],[RETRIGGER-POST],[WAVELOOP],[DOORSND],[VOICEMAP],[MISSIONSTATE],[MUSICNOTE],[EVTQ-DROP],[PASTEND]). KEEP[EXPIRE]— part of the M-66b fix, not a probe. Keep[VOL]/[DISTVOL]until D205 step 2 is done. Trim GE-ENV-PROBES.md; set the D202 index status to CLOSED; check README/AGENTS phase-status lines for "D202 open" wording. - Build + smoke after probe removal:
./build-pc.sh ntsc-final, then-level_0960 s crash-free withGE_AUDIOTRACE=1.
Environment (unchanged except noted):
- Build:
/c/msys64/usr/bin/bash.exe -lc 'cd <repo> && ./build-pc.sh ntsc-final'(login shell for MINGW64 PATH; SDL2.dll at/c/msys64/mingw64/bin). - Capture run:
GE_AUDIOTRACE=1 GE_INPUTSCRIPT="$(cat ../scratchpad/exact_run3_script.txt)" ./ge007.x86_64.exe -level_09frombuild-pc/; kill withtaskkill //F //PID <pid>(msyskilldoes not work). mupen64/(N64 emulator, user's A/B reference) at repo root — added to .gitignore this session.scratchpad/is gitignored: banktest/, exact_match.py, fire scripts, match outputs are all local-only. To re-verify the bank exoneration:scratchpad/banktest/harness.exe(rebuild from harness.c against port/src/romdata.c if stale) thenpython scratchpad/banktest/diff_bank.py.- Matcher constants:
DEC_BASE=0x706d64c0, PV stride 0x158,ENV_BASE=0x706d8540, env stride 0xb0; pitch = 2^((keyBase*100+detune-6000)/1200); WIRE lines are inaudiotrace_wire.log, NOT audiotrace.log; state addresses print as zero-padded 16-hex without0x. - Uncommitted at handoff time (all probe/docs, nothing behavioral): findings.md (M-71 + D205), HANDOFF.md, GE-ENV-PROBES.md, .gitignore, src/snd.c (
[VOL]), src/game/propobj.c ([DISTVOL]), plus the earlier-session probe set still in place (port/src/libultra.c geTracePrintf, port/src/romdata.c, src/game/mp_music.c[MISSIONSTATE]/[MUSICNOTE], src/libultra/audio/{csplayer,event,load}.c) and untrackedport/include/audiotrace.h— commit them together with this handoff.
Done this session (M-69) — D202 full-corpus exact match: 320 scripted-run SFX requests matched per-voice; 286/320 OK, no corruption found.
Full write-up: findings.md D202 §M-69. scratchpad/exact_match.py (with
rom_sfx_decode.py / m63_crosscheck.py) matches every sndPlaySfx request
in a full trace (scratchpad/exact_run/: scripted -level_09, 61 input
entries, GE_AUDIOTRACE + WIRE + GE_VOICEDUMP) to its voice's voicedump
stream and slides the ROM-decoded PCM against it.
- 286/320 OK (CC ≥ 0.85). The first run's 44 "BAD" were tooling, not audio:
(1)
GE_VOICEDUMPµs stamps are block-END times (records render ahead — per-sample record time makes sound 29's onset land +263.8 ms vs WIRE +264 ms, exact); (2)slide()needed step-1 refinement around the coarse best hit (sharp onsets: CC 0.590 → 0.970 one sample later); (3) refs must be clipped at voice death ([EVT]DEACTIVATE/STOP after PLAY — idx=109 then CC 0.999). - Remaining ~34 failures are not corruption: requests with no
[VOICE+](allocation dropped — e.g. t=567204976 idx=46; suspected 8-voice pool exhaustion under rapid fire, N64 would drop them too) + a few pv10 misattributions where consecutive requests share a wavetable and the wire window catches the next request's wire. - Log-interleaving gotcha: AUDIOTRACE lines from multiple file handles corrupt some lines; the matcher falls back to a wide wire window when the PLAY line is unreadable.
Next (in order):
- Verify the dropped requests are pool-exhaustion drops: check
[VOICES] allocated=N / max=8inexact_run/audiotrace.logaround each "NO WIRE AT PLAY" timestamp; if the pool is full every time, confirm the steal/drop path inalSynAllocVoice(src/libultra/audio/synallocvoice.c, ~512-sample steal delay) is faithful and close that thread as N64-parity. - Tighten wire attribution: WIRE must fall between this request's PLAY and the next PLAY on the same wavetable (kills the pv10 shared-wavetable cases).
- Then M-68's remaining items: user by-ear confirmation + N64 A/B on the door-close-loop duration (M-66b cap), remove the D202 probe set, close D202.
Committed: all of the above is on branch feat/phase3-audio
(PR #28), 2 commits — 31935ad9 (probes: GE_VOICEDUMP, [VOICEMAP],
timestamped [WIRE]/[EVT]/[STOP-EVT], -VoiceDump) + 1ac92c7d
(findings M-67/M-68/M-69 write-ups, GE-ENV-PROBES entries, AUDIO-PLAN status
banner). Build verified green (ntsc-final). Do the next steps above on
feat/phase3-audio, not main.
Done this session (M-66b) — D202 disposition C IMPLEMENTED + measured: ownerless infinite-loop SFX now expire on PC. Awaiting user by-ear pass, then probe removal + close.
User chose C (extended deviation), as part of the PC port's audio implementation. M-65's guard stays (slot reclamation under pool pressure); this adds the audibility fix. Full write-up: findings.md D202 §M-66b.
What changed (src/snd.h + src/snd.c, all #ifdef PORT, N64 zero diff):
new PC-only event AL_SNDP_PORT_EXPIRE_EVT (reuses the unused 1<<13 slot).
In PLAY_EVT, after a voice starts, if it is provably ownerless
(LOOPED + FINAL + not RETRIGGER + state->state == NULL) and its wave
carries an ADPCM loop with count == -1, post the expire at 2 s. Handler
re-validates the predicate, ramps volume to 0 over 500 ms, posts END_EVT.
Constants are in microseconds (ALMicroTime — the queue is µs-driven).
Not a reused STOP_EVT: its ramp uses the envelope releaseTime (~2.7 ms for
sound 203) and would hard-cut/click. Stale-event safe because
sndDisposeSound already removes ALL pending events for a state
(sndRemoveEvents(..., 0xffff)); owned loops and retrigger sounds are
untouched. Placed in snd.c (not port/src/mixer.c) because ownership is
only visible in sndp — the mixer has no ownership field, and scoping there
would force region-fragile DRAM pointer ranges or capping all infinite-loop
voices (music included).
Measured (134 s headless -level_09, GE_AUDIOTRACE=1 GE_AUDIODUMP=1):
every sound-203 play posts [EXPIRE]; each leaked voice gets its VOICE- ~2 s
after its VOICE+; run ends allocated=0 / max=8. Per-1-s Goertzel at 256.7 Hz
(the strongest 203 line × 0.7278): pre-fix user capture = flat at max
t≈39→361 s (the complaint verbatim); post-fix = transient ~2–4 s bursts
with full silence between (gaps t=60–62, 85–87, 94–98, 110–112, 128–131).
Stuck infinite loop is gone; each close rings ~2 s + 0.5 s fade. Scripts +
archived user capture: scratchpad/d202-m66/ (check_dump.py,
line_track.py, user-capture-audiodump.raw, user-capture-audiotrace.log).
Next (when the user gives the by-ear OK): remove the D202 probe set
(GE_AUDIOTRACE additions, [DOORSND], [SLOTWRITE], GE_PULLTRACE,
GE_DMEMWIPE, GE_NOWET, GE_BANKDUMP, GE_KEYMAPDUMP) from
src/snd.c, src/game/propobj.c, src/libultra/audio/load.c,
port/src/romdata.c, port/src/mixer.c; update GE-ENV-PROBES.md; close D202.
The [EXPIRE] line and the guard/expire code STAY (permanent, documented).
Build note for this shell: toolchain at /c/msys64/mingw64/bin, and
cmake --build -j hit "Cannot create temporary file in C:\Windows\" —
ninja -j1 with a writable native TMP/TEMP works.
Done this session (M-66) — D202 ROOT CAUSE ESTABLISHED: the audible stuck door loop is sound 203 behaving exactly as ROM + ground-truth code specify — faithful N64 behaviour (original quirk), not a port bug. Disposition A/B/C pending user decision.
The user capture M-65 asked for arrived (build-pc/audiodump.raw 361.7 s +
build-pc/audiotrace.log, -level_09 start with the computer-room double
doors' attract intro). Full write-up: findings.md D202 §M-66. Chain, every
link grounded:
- ROM: sound 203 (
METAL_SLIDE_CLOSE_SFX) wavetable loop(2471, 7719, count=-1)= infinite (libultraload.c: "-1 is loop forever"); envelopedecayTime=-1. - Game code (byte-matched):
doorPlayCloseSound0/1plays it fire-and-forget, NULL owner;[SLOTWRITE]proves the state is never stored in any door slot → nothing ever deactivates it except level exit. - sndp: preemption skips flag 0x12 (LOOPED|RETRIGGER);
AL_SNDP_DECAY_EVTskip stop-scheduling for looped sounds;decayTime=-1→ priority 0x41, unstealable by_allocatePVoicefrom any 0x40 sound. Pool: 24 PVoice (MUSIC_SYN_CONFIG_MAX_P_VOICES=0x18) + GE soft limit 8. - Result: every metal-door close leaks a looping voice; during attract idle nothing evicts it → endless door loop, exactly the user's report. All code paths are shared original source → identical on N64.
327 ms mystery solved: the voice plays at -550 cents (keyBase 54 + detune 50 − shift 6000 → ratio 0.72783); 5248 samples / (22050×0.72783) = 327.007 ms vs observed 327.0±0.1 ms. Spectral lines match ×0.7278; pitch-stretched template xcorr r=0.835 (unpitched was 0.10 — why all earlier template tests "failed").
Corrections to M-65 banked: the "~1400 RMS drone is background music, not a stuck sound" correction was wrong/incomplete (steady state = leaked 203 loop
- music composite; the loop dominates the fingerprint); "stuck loop not reproduced" is now superseded. Timeline corrected too: SFX do continue past t=39 s — door …80a4 re-closes at t≈360.7 s (5th leaked voice); four leaked 203 voices alive by t≈40 s; M-65's guard fired twice (t=14.8, 16.4 s) reclaiming two under pool pressure.
Decision (user): C chosen — extended deviation, cap/expire ownerless loops to silence the idle loop. Implemented + measured in M-66b above. (A) pure fidelity — remove the M-65 guard; (B) keep the guard only — were the other options. Once the user's by-ear pass lands: remove the D202 probe set (see GE-ENV-PROBES.md) and close D202.
Done this session (M-65) — D202 PARTIALLY resolved: voice leak root-caused + guarded, sndCreatePostEvent un-stubbed (distance attenuation restored). Audible stuck door loop still NOT reproduced. Session was cut by the usage limit AFTER the findings.md write landed — everything below is recorded in full in findings.md M-65.
1. Voice leak (the "eventual silence" cascade) — CONFIRMED, MEASURED, guarded.
soundIndex=203 (METAL_SLIDE_CLOSE_SFX) has envelope->decayTime == -1
(genuine ROM data — verified at the source-bank level with new GE_BANKDUMP/
GE_KEYMAPDUMP probes) → SOUND_FLAG_LOOPED → never posts STOP_EVT, and the
preemption scan refuses to steal looped voices. doorPlayCloseSound0/1 play it
with a NULL owner, so nothing can ever deactivate it. Measured on 115 s of
headless -level_09: 7 of 8 voices permanently held by sound 203, leaving
one usable voice for the whole game — every later SFX gets a state but no
voice and is dropped. Fix: 4-line guard in the src/snd.c preemption scan
reclaiming only provably-ownerless loops (LOOPED && !RETRIGGER && state->state == NULL). Measured: acquisitions 171 → 629, count no longer
pinned at 8, residual stuck voices 7 → 1. User confirms "less cascading".
This is a rule-#2 game-code change justified by measurement, not ground
truth — kept flagged as a documented port-side deviation.
2. sndCreatePostEvent un-stubbed (D138) — FIXED. The D138 stub had been
silently removing ALL distance-based volume attenuation (every caller posts
AL_SNDP_VOL_EVT). Its O(n²)-watchdog premise expired when the Phase-3 audio
thread landed. Removed; re-measured clean (rt=0.996, drop 0, no watchdog
trips over 115 s). Likely a large part of the "wrong/too-loud/multiple sounds"
impression.
3. Audible endless door loop — still NOT reproduced. Ruled out by
measurement (do not re-walk): wave-level looping (loop==NULL on every sound),
decoding past wave end ([PASTEND] 0 hits), stale/accumulating DMEM (full
per-frame wipe → unchanged), undamped reverb feedback (GE_NOWET → unchanged),
retrigger posts (0), door oscillation (~4 cycles/door/110 s, normal).
Corrections banked: the romdataFixupMusicSeqTable ... capacity 1 error is
BENIGN (deliberate first call); the ~1400-RMS "drone" in AUDIODUMP captures is
background music (csplayer.c:535), not a stuck sound.
Next step (what the session ended asking for): user capture at the failing
door (computer-room double doors, or a level start whose attract intro shows
them opening) with GE_AUDIOTRACE=1 GE_AUDIODUMP=1 ./ge007.x86_64.exe -level_09,
then send build-pc/audiotrace.log + build-pc/audiodump.raw. The dump lets
the stuck sound be isolated numerically (separated from music) and pinned to
the exact door event via dumppos. All diagnostics are env-gated — catalogue in
findings.md M-65; remove when D202 closes.
Done this session (M-64) — D204 FOUND + FIXED: PC audio ran ~2 % below real time permanently. D202 still OPEN; two M-63 conclusions corrected.
New finding D204, fixed and measured. Found by reviewing the audio pacing loop — the whole M-56→M-63 chain had been auditing the data path.
The defect: amMain wakes at 30 Hz (sched.c:334 forwards every 2nd
retrace to the audio client, audi.c:441) and asks for g_MinFrameSize=720
samples per block = 21600/s against a 22050 Hz device — structurally 2 %
short. audi.c:531 relies on 784-sample top-up blocks to make it back, but
only requests one once the reported AI length drops under ~69 frames = 3 ms.
Fine on N64 (double-buffered AI, exact VI interrupt); on PC ordinary OS
scheduling jitter empties a 3 ms cushion before the loop reacts, the SDL queue
hits zero, and the device pads playback with silence — unrecoverable time, a
queue-depth reading cannot tell the regulator it already fell behind.
Measured rt=0.980 sustained with q repeatedly at 0, indefinitely, on every
level, for every user, since the first frame.
The fix (F5, port-only — src/audi.c untouched, rule #2 clean):
audioGetAiLengthBytes() in port/src/audio.c subtracts a 1024-frame
(~46 ms) cushion before reporting, so "queue holds the cushion" reads as
"queue is empty" and the loop tops up while slack remains. Also landed: F1
correct single-buffer AI_LEN_REG semantics (robustness — measures as a no-op),
F3 Audio.QueueLimit 8192→2880 + non-silent drop logging, F4 oversize
invariant guard.
Measured, in-binary A/B via GE_D204_OLD=1 (same executable, no
build-to-build variance), -level_09 scripted PPK fire:
| rt | q (min..max) | drop | max block | |
|---|---|---|---|---|
| before | 0.980 | 0..416 | 0 | 3136/3156 |
| after | 1.000 | 368..672 | 0 | 3136/3156 |
5-minute soak on the shipping binary: rt=0.999, drop=0, q never near 0.
Not yet verified by ear — a human listening pass is still owed.
New tooling — . ools_pcudiodebug.ps1, the audio counterpart to
debug.ps1 (that one is for crashes). Drives every audio probe and prints a
pass/fail verdict (real-time ratio, queue starvation, dropped/oversized blocks,
soundIndex histogram):
. ools_pcudiodebug.ps1 -AB -Fire # before/after table - how D204 was measured
. ools_pcudiodebug.ps1 -Play -Trace # instrumented interactive playtest (D202)
. ools_pcudiodebug.ps1 -Soak # 5-min stability run
. ools_pcudiodebug.ps1 -SyncData ... # mirror ./data into build-pc/data first
Two harness gotchas it encodes, both of which cost a debug cycle and are
written up in findings.md D204: Start-Process -RedirectStandardOutput cannot
capture this game's stdout (it is WIN32_EXECUTABLE/-mwindows, no console),
and MSYS2 bash.exe launched from PowerShell arrives with a stripped
environment so every GE_* probe reads as unset — the script generates a
.bat and runs it under cmd.exe instead.
Underlying probe — GE_D204=1, an audio-health monitor (one line / 5 s:
rt= real-time ratio, q= queue depth, drop=, max= block vs allocation).
Deliberately cheap (~30 clock reads/s) so it can be left on for a whole
playtest — this is the instrument D202 actually needs.
Two M-63 conclusions corrected — do not pursue either:
soundIndex=109is a red herring, closed negative. The "94 calls" is double-counted (the probe logs 2 lines per call) — 47 real, all in bursts after t≈5.4 s interleaved with ricochet indices = guards returning fire, correct behaviour. The "suspicious uniform ~32.6 ms cadence" is just the audio-block quantum (720 frames × 4 = 2880 bytes) that everydumpposin the trace is rounded to.- M-63's "8 s of audio in a 60 s run" was its own probe's artifact.
GE_MIXERTRACE=1's unbuffered per-opcodefprintf(25 MB log) slows the process enough to starve the audio thread. Same repro without it: 58.0 s / 60 s.GE_MIXERTRACEis unsafe for any timing-sensitive measurement — M-63's advice to have the user replay with it set would have induced the very starvation being hunted. UseGE_D204instead.
A hypothesis I raised, tested and FALSIFIED — do not re-open. I predicted
the u32 subtraction at audi.c:531 wraps past 789 queued frames, truncates to
u16 (58128), and drives a 74× heap overrun of the 3156-byte info->data. The
wrap is real (high-water 2064 frames measured) but harmless:
frameSamples is declared s16 (audi.c:145), so it lands negative and
audi.c's own lower clamp catches it. Max block stays 3136 B vs the 3156 B
allocation in every run, both modes. No overrun. This is why F1 measures as a
no-op.
D202 is NOT resolved by this and stays OPEN. Nothing degraded in any
headless run this session (5 min soak clean, no voice-pool growth). Its
reported chain — wrong PPK sound, correct one sometimes heard too, pile-up,
eventual near-silence except a stuck loop — still needs a real playtest.
Next session: have the user play with GE_D204=1 GE_AUDIOTRACE=1 (and
not GE_MIXERTRACE) and hand back ge007.log + audiotrace.log; rt
falling or q pinning at queueLimit with drop= climbing localises it
immediately. Do NOT re-open the index/data/decode/ROM-offset chain (M-56–M-62,
solid) or the 109 lead.
Uncommitted: the D204 fix (port/src/audio.c, port/src/libultra.c,
port/include/audio.h) plus the pre-existing D202 diagnostic probes. Full
detail: docs/dev/findings.md D204.
Done this session (M-63) — D202 REOPENED again: user's real complaint is runtime mixer/voice corruption, not a wrong-sample-data question. Live repro built; root cause not found; one concrete anomaly banked.
Supersedes the M-62 "recommend close" below — after that was written, the user clarified the actual bug: the wrong ("slap") sound always plays on the silenced PPK, the correct suppressed shot sometimes plays too (both heard together), and over a play session sounds "pile up and spirally glitch out" until eventually no audio plays except a looping sound (e.g. Bunker's door) stuck since level start. That's real-time mixer/voice-lifecycle corruption, categorically different from "wrong soundIndex requested" — the M-56–M-62 chain (which is solid) never actually tested this.
Build+run works in this environment: export PATH=/c/msys64/mingw64/bin:$PATH (MSYS2 MINGW64 gcc/cmake/SDL2), then
./build-pc.sh ntsc-final from repo root. To run, build-pc/data/ needs
the full asset set, not just the ROM: copy data/pccg-ntsc-final/,
data/pcmodels-ntsc-final/, data/ge007.ntsc-final.z64, and (to match the
user's real state) data/ge007.eep+data/ge007.ini into build-pc/data/.
A costly false alarm this session: skipping the sidecar dirs makes
-level_09 hard-crash in load_bg_file almost immediately (raw big-endian
ROM read instead of the converted PC sidecar, per obInit()'s own comment)
— looks exactly like a scary new engine bug, cost a stashed rebuild + a gdb
session to rule out, and was purely a test-setup gap. The [WARN] pcmodels.bin not found / [WARN] pccg.bin not found lines are printed
right there in the log — read them before chasing a crash. Next session:
always mirror the full data/ dir, verify with those two warnings absent.
Repro used: GE_MIXERTRACE=1 GE_AUDIOTRACE=1 GE_AUDIODUMP=1 GE_INPUTSCRIPT="600:R,Z;601:R,Z;...;3210:R,Z;3211:R,Z" (30 fire pulses,
~0.75s apart) timeout 60 ./ge007.x86_64.exe -level_09. Ran clean 60s, no
crash. Logs saved: scratchpad/d202-m63/{audiotrace.log,mixertrace.log, audiodump.raw} (mixertrace.log is 25MB — grep it, don't read it whole).
Ruled out: voice-pool exhaustion/leak. Only 16 distinct voice slots used
across 153 sndPlaySfx calls in 60s, properly recycled; sndDeactivate
count (159) tracks sndPlaySfx count (153) almost 1:1. The D199/M-53–M-55
"voices never freed" lineage does not reproduce here.
Found, unexplained — top lead for next session: soundIndex=109
("109_GUN_B4_BOLTACTION_SFX", //used for AK47, bondconstants.h:2546)
fired 94 times in 60 seconds with no AK47 anywhere in the repro (PPK
only), and part of that traffic retriggers the same voice slot at a
suspiciously uniform ~32.6ms cadence (2880 bytes in the mixed stream,
4-in-a-row) — far faster than any plausible weapon cadence in this game.
Reads like something re-issuing sndPlaySfx every audio tick instead of
once per game event, which would explain "sounds piling up." Not yet
confirmed as the cause of the user's exact symptom (109 isn't the PPK's
sound), but it's the most concrete, checkable thing this session produced.
Not reproduced: the user's full described chain (pile-up → glitch → total silence except one stuck loop). The 60s/58-shot synthetic burst stayed healthy. Either it needs a longer/denser real-play session, or the soundIndex=109 anomaly is the seed that compounds given more time — chase (1) below first, it's cheaper than an extended blind playtest.
Next session, in order:
- Find what calls
sndPlaySfx(109, ...)insrc/game/*.cand determine if the ~32.6ms retrigger is legitimate (real per-round automatic-fire loop, just fast) or a bug re-firing every audio tick instead of once per event. - If that doesn't explain it, build a much longer repro (5+ min, walking +
intermittent combat) or have the user reproduce with
GE_MIXERTRACE=1 GE_AUDIOTRACE=1 GE_AUDIODUMP=1set and hand back the logs, aimed at reaching the "total silence except one stuck loop" end state; diff voice-slot reuse timing against this session's healthy 60s baseline. - Actually listen to
scratchpad/d202-m63/audiodump.raw(raw s16 stereo 22050Hz PCM) around the soundIndex=109 retrigger timestamps — not done this session, log-analysis only so far. - Do NOT re-open the index/data/decode/ROM-offset chain (M-56–M-62) — that part is solid; this is a different mechanism.
No code changes this session (build+run+trace only; the pre-existing
uncommitted diagnostic files were exercised, not edited). Full detail:
docs/dev/findings.md D202 M-63 entry.
Done this session (M-62) — D202: byte-match verification confirmed to cover the struct + post-dates the source; user confirms the M-60 reference clip's provenance is unknown/found-online. SUPERSEDED BY M-63 — see above; the "recommend close" below did not hold up.
Read-only session, no code changes. No MIPS toolchain is available in this
environment (mips-linux-gnu-* absent) so M-61's item 1 (build the N64
target, read the map/ELF for the literal ROM byte) wasn't attempted — went
for the cheaper item 2 instead.
Found scripts/ge007.u-test_basis.csv:946:
0289c36e967840bba7f6fbd026dda001,.data,build/u/src/game/gun.o —
test_files.sh/test_files_readme.md confirm this md5 is a known-good
checksum of gun.o's compiled .data section, which is where
wppksil_stats lives (gun.c #includes gunWeaponStat.inc.c). This
directly falsifies M-61's alternative (a) ("verify doesn't cover this
segment") — it does.
Timing check: git blame -L946,946 on that CSV → commit cc14d64e7
("for england james?", 2026-08-16, upstream sync). git log on
assets/obseg/gun/wppksil/gunWeaponStat.inc.c → last touched 9fbe1fd5,
an ancestor of cc14d64e7. The checksum postdates the source's last
edit — it isn't testing something since changed. Re-read the file:
.Sound is still 0x2E. Since AGENTS.md rule #2 means the PC-port work
never touches src/game/gun.c / assets/obseg/gun/**, nothing has
drifted since that checksum was captured.
Also found: this repo's own CI (.github/workflows/ci.yml) never builds
the N64 target — validate/linux-build/windows-build are all PC-port
jobs, no test_files.sh/N64 Makefile invocation anywhere. The "byte-
matches ROM" claim in AGENTS.md is inherited from upstream
n64decomp/007's own history, not continuously re-verified in this repo.
Worth knowing generally; orthogonal to D202.
Net effect: this is strong circumstantial evidence, not a rebuild-
confirmed "pass" (that still needs installing a MIPS toolchain and
literally running scripts/test_files.sh) — but combined with M-61's
already-closed alternative (c) (no runtime remapping), the two live leads
left are (b) reference-clip provenance and (d) envelope-match
reliability, both pointing away from a port bug.
Asked the user directly (same session): where did B00I00S2D.wav come
from? Answer: "Not sure / found online." That was the last unverified
link — five sessions (M-56–M-62) have independently verified every other
part of the chain (index resolution, bank order, pointer chain, ADPCM
decode math bit-exact, ROM offset, and now the source data via the
byte-match checksum). Recommend closing D202 as NOT-A-BUG — the same
conclusion M-59 reached, now on much stronger evidence.
Next session, in order: (1) confirm with the user whether to close
D202 outright, or run the cheap optional emulator A/B first (fire the
silenced PPK on the real ROM in a known-good N64 emulator, listen) as
final belt-and-braces confirmation before closing; (2) if the user instead
wants a hard rebuild-confirmed pass rather than the strong circumstantial
evidence, install a MIPS toolchain (binutils-mips-linux-gnu equivalent)
and run scripts/test_files.sh for real; (3) once D202 is closed, resume
the roadmap's B3 audio track / ROADMAP-1.0 priorities — this whole seven-
session investigation was a detour from the plan-of-record ordering
(B1 → B2 → B3 → …).
Full detail: docs/dev/findings.md D202 M-62 entry (+ the same-session
follow-up appended just below it).
Done this session (M-61) — D202: both M-60 threads resolved; keystone fact is now "what byte does the ORIGINAL ROM hold at wppksil_stats.Sound". STILL OPEN.
Diagnostic-only session (no game/port code touched). Both live threads from M-60 are done:
- Bank-navigation off-by-N hypothesis: CLOSED (negative).
port/src/romdata.c's bank re-layout walksALBankFile -> ALInstrument -> ALSoundin exact ROM order; PC index N == N64 index N. Dead end — do not re-litigate. - Offline decoder: FIXED + VALIDATED.
scratchpad/rom_sfx_decode.pynow matchesaADPCMdecImplexactly (fixed swapped tbl0/tbl1, missing intra-subframe accumulation, wrong ncoef). Full 261-sound bank scan: index 45 (DROP_GUN_SFX) is the only strong match for the user's reference clip (envcorr +0.999); index 46 (GUN_SILPPK_A_SFX) is the "slap" (scratchpad/romsfx_44..60.wav). - Fire path traced:
gunfire.cL3196/3200 →bondwalkItemGetSound()(gun.cL1343) → raw u16WeaponStats.Sound(offset 38). No runtime remapping. Both N64 and PC builds compile the same initializer from source (gun.c:135includes the aggregategunWeaponStats.inc.c; per-weapon file is an identical second copy) — both say.Sound = 0x2E(46). Unsilenced PPK uses 0x6B.
Keystone logical constraint (new): the N64 build byte-matches the
original ROM, and this data is compiled from source in that build — so a
transcription error in .Sound would have broken the match. 0x2E is
therefore almost certainly what the original ROM holds, meaning real N64
hardware also requests index 46. That contradicts the reference clip
matching index 45, so exactly one of these is false: (a) byte-match verify
doesn't cover this data segment; (b) B00I00S2D.wav isn't an in-game N64
capture (filename looks like a media-library asset ID — provenance never
stated); (c) N64 runtime resolves 46 differently than ROM order; (d) the
+0.999 envelope match is misleading.
FAILED this session — do NOT repeat: direct ROM byte-pattern searches
for the struct (looped 40+ times, all 0 hits: decompiled floats don't
round-trip to IEEE-754 bits, integer tails also absent) and a hand-computed
RAM→ROM mapping (yielded offset 0xC11934 > 12MB file size; .csegment
sits at ROM 0xC00000 = EOF in ge007.ld, so the arithmetic was wrong).
No N64 .map at maxdepth 3 (only PC CMake tree build-linux/).
Next session, in order:
- Find the N64 build map file or ELF (search deeper than maxdepth 3:
assets/obseg/Makefile.*,ld/,dist/; thennm/objdump) and read the actual 2 ROM bytes atwppksil_stats.Sound(vaddr0x800326C4+ offset 38). ROM=0x2D → transcription error (audit why verify missed it); ROM=0x2E → ground truth, pivot to (b)/(c). - Check the byte-match verification tooling: does it compare data segments or code only? Find the last full-verify output.
- Ask the user one question: where did
B00I00S2D.wavcome from? - Cheap corroboration: live PC trace of the soundIndex passed to
sndPlaySfxon PPK fire (expected 46).
Full detail: docs/dev/findings.md D202 M-61 entry.
Done this session (M-60) — D202 REOPENED: user rejects "ground truth, not a bug" and supplies a real reference clip. STILL OPEN, do not close.
User's own words: "I know for a fact its the wrong sound playing in the
PC port specifically." This directly overrides M-59's conclusion below —
do NOT treat D202 as closed/WONTFIX. The user also dropped a real reference
recording of the silenced PPK's actual fire sound: scratchpad/ B00I00S2D.wav (untracked/scratch, mono/16-bit/16kHz, 0.204s). Its RMS
envelope is sustained (stays high for ~80% of the clip) vs.
d202_soundindex46_isolated.wav's early-peak-then-fast-decay shape —
consistent with, though not proof of, the user's "slap not gunshot" call.
Two live threads, neither finished — pick up here next session:
- Bank-navigation-bug hypothesis (new, not yet checked): soundIndex 46
is named
46_GUN_SILPPK_A_SFXinbondconstants.hyet decodes to what the user twice ID'd as the melee-slap sound (whose own indices are the adjacent 47/48/49_PUNCH*_SFX). A correctly-resolved sound shouldn't contradict its own debug label — check whetherport/src/romdata.c's bank re-layout (afCtx,_bnkfPatchWaveTable) walksALBank->ALInstrument->ALSoundin the exact ROM order, i.e. whether the PC conversion could be off-by-one/off-by-N vs.src/snd.c's documented resolution pathsoundBank->instArray[0]->soundArray[soundIndex]. Not checked line-by-line against actual ROM bytes for this bank yet. scratchpad/rom_sfx_decode.py(WIP, untracked, NOT working yet) — a fresh standalone ROM bank parser meant to decode a range of soundIndex values and diff each againstB00I00S2D.wavto find which one actually matches. Its ADPCM predictor loop is currently wrong (stubbed intra-subframe accumulation). The correct formula, confirmed fromport/src/mixer.c'saADPCMdecImpl(L185-239, ground truth): table shapesAdpcmTable[predictor][2][8], per samplejin a subframeacc = tbl[0][j]*prev2 + tbl[1][j]*prev1 + (ins[j]<<11) + sum_{k<j} tbl[1][j-k-1]*ins[k]. Fix the script to match this exactly, then rerun across a wider soundIndex range.- Also flagged but unresolved: M-59's
d202_soundindex46_correctpitch.wavresample experiment looks direction-inverted (fewer output samples than input at a pitch ratio that should stretch, not shrink) — don't trust it until re-derived againstaResampleImpl; not sent to the user.
Full detail: docs/dev/findings.md D202 M-60 entry.
Done this session (M-59, updated) — D202: soundIndex 46 (silenced PPK's fire sound per decompiled ground-truth weapon data) genuinely IS the melee-slap SFX, per direct user listening test on an offline ROM-only decode. Every step of the pipeline traced to real N64 weapon-stats data — no port bug found. Recommend an emulator A/B as the tie-breaker before closing or touching any code.
Correction to the note below: the initial "ricochet-free clip" A/B test
was flawed — fire (46) and ricochet requests land at the same audio-frame
position (dumppos, a new exact byte-offset marker added to
GE_AUDIOTRACE via port/src/audio.c's audioDumpBytePos()), so an
RMS-peak-based clip extraction can't isolate one from the other; both
clips likely had ricochet content, just at different volume. The user
confirmed both clips sounded like the same "ricochet/slap" sound.
Definitive isolated test: decoded soundIndex 46 entirely offline
(raw ROM bytes at file offset 0x03128a0, the M-58 VADPCM decoder, zero
live game/mixer involved — no possibility of overlap) → scratchpad/ d202_soundindex46_isolated.wav. User: "yeah that is the slap effect that
in the original game plays when you do a melee attack."
Traced why 46 is requested at all: bondwalkItemGetSound() →
WeaponStats.Sound (src/game/gun.h:87) → the silenced PPK's stats
(assets/obseg/gun/wppksil/gunWeaponStat.inc.c, a static, hand-
decompiled data table, not a runtime ROM conversion) has
.Sound = 0x2E = 46, positionally. This is ground-truth decomp data
(//D:800326C4 RAM-address comment), covered by the project's byte-match
build verification — not something a PC-side conversion bug could produce.
Conclusion: real N64 hardware would request the same soundIndex 46 for
this weapon. Every link (weapon data → bank index resolution → ROM bytes
→ ADPCM decode) is independently verified correct and N64-identical. Either
this is genuinely how the shipped game sounds (asset reuse between the
silenced pistol and melee, not unheard of under ROM budget), or there's a
rare transcription error in wppksil_stats.Sound that survived this
project's byte-match verification (would need the linker map to check the
true ROM offset of this struct — not done). Not fixed, nothing committed
— did not touch gunWeaponStat.inc.c (ground-truth decomp data,
speculative "correction" is out of scope without independent ROM proof).
Next session, if pursuing further: run the same ROM in a known-good N64
emulator, fire the silenced PPK, listen. Matches slap → close D202 for good,
not a bug. Sounds like a normal gunshot → the .Sound field is the one
concrete lead left, fixable as a single-field data correction gated by the
existing byte-match verification.
Done this session (M-59) — D202 likely RESOLVED: not a bug at all — the "ricochet + melee slap" is a real, separate, correctly-triggered SFX (bullet hits a wall in the repro), not corrupted gunfire audio
Continued straight from M-58. Two checks, in order:
- Byte-exact ROM verification. Read
data/ge007.ntsc-final.z64directly at cart offset0x03128a0(= base 0x103128a0 - CART_BASE 0x10000000) with a throwaway Python one-liner — matched M-58's live-traced DMEM bytes (900de113224092ecde90f602424ae1bf...) exactly. Traced why this is a valid direct-ROM-offset check at all:_sfxtblSegmentRomStart(port/src/romassets_u.s:2948) is a linker constant equal to the literal cart address0x102F19A0, passed straight throughalBnkfNew→_bnkfPatchWaveTable'sw->base += table(src/libultra/audio/bnkf.c) — GE's wavetable sample data (unlike the bank-structure metadata) is never relaid-out byport/src/romdata.c; it points straight into the live cart-mapped.z64image. Last theoretical gap in the M-56/M-58 data-integrity chain is now closed at the raw-byte level. - Re-read the full
audiotrace.log, not just soundIndex 46/122. Cross-referenced every other soundIndex againstbondconstants.h's"NN_NAME_SFX"string table (index baked into the name). Every single fire (46) request in the trace is immediately followed by one of{23, 24, 25, 37}=RICO_6_TAJ_A/B/C_SFX/RICO_5_C_SFX— ricochet impact sounds — cycling per shot. Found the call site:src/game/gunfire.c'srecall_joy2_hits_edit_detail_edit_flag()(unmodified ground-truth bullet-hit handler): any hitscan that strikes a non-character prop unconditionally plays a random ricochet SFX from a 20-entry table. This is exactly what GoldenEye does when you shoot a wall, on real N64 too.
Conclusion: the M-52/M-56 repro's fixed aim direction has the player shooting straight into a nearby wall the whole time, so every shot legitimately plays THREE correct sounds at once — fire (46) + cartridge- eject (122) + a random ricochet (23/24/25/37) — not one wrong one. The "sounds like ricochet + melee slap" description matches a ricochet impact's character far better than a coincidental bug that always produces two other real, nameable sounds. Three full sessions (M-56/M-57/M-58) of independent bit-exact/byte-exact verification found nothing wrong anywhere in the chain, which is now explained: there was nothing to find.
Not yet proven: whether the fire SFX itself is perceptually prominent
enough / correctly balanced against the (real, expected) ricochet — could
just be masked by a louder transient, which is normal. Recommended next
session, before resuming M-58's envmix/PVoice trace: rerun the same
GE_AUDIODUMP capture with the aim pointed at open air (no wall hit), confirm
via audiotrace.log no ricochet_sounds_small index fires, and have the
user listen to that clip specifically. If fire+cartridge alone sound correct
and recognizable, close D202 as a repro artifact, not a port bug. Only
resume the envmix/PVoice-identity trace if that clip still sounds wrong.
Done this session (M-58) — D202: address/DMA plumbing AND the ADPCM decode math both independently verified bit-exact correct; bug is downstream (envmix/output-routing or PVoice identity), still OPEN
Picked up exactly where M-57 left off (priority (1): decoded-PCM-per-voice
dump). Extended GE_MIXERTRACE (still env-gated, zero cost when unset) to
log DMEM source bytes, ADPCM book coefficients, and the first decoded frame
in port/src/mixer.c; added [DMAREQ]/[BINDTABLE] in
src/libultra/audio/load.c and [DMAHIT]/[DMAMISS] in src/audi.c's
amDmaCallback — all four now write into the same mixertrace.log so one
physical voice filter's whole lifecycle reads chronologically in one file.
Made and caught a real mistake mid-session: first compared
wavetable->base from one run's audiotrace.log against aLoadBuffer
addresses from a different run's mixertrace.log (separate files, no
shared ordering) and momentarily concluded the mixer never touches the ROM
wavetable data at all — a false alarm from cross-run/cross-file
correlation, not a real bug (per-run nondeterminism, D117, shifts line
counts between runs). Fixed by merging every new probe into one trace file
and rerunning once. Lesson banked in findings.md M-58.
With everything in one file, followed soundIndex 46 (the fire SFX) end to
end for one live occurrence: BINDTABLE → DMAREQ (memin exactly matches
the bound table's base, no address confusion) → DMAMISS (legitimate cold
ROM→staging-buffer copy via piServiceDma, a real cart address under
romdata.c's mapping) → LOADBUF (dumped the actual ADPCM source bytes)
→ ADPCMDEC (dumped the loaded book coefficients, matching sndPlaySfx's
own book pointer for that soundIndex) → PCMOUT (dumped the first decoded
16-sample frame). Hand-decoded the same 9 source bytes against the same
book coefficients using the textbook VADPCM algorithm, entirely independent
of this codebase's code — first three samples matched the C output
bit-exact (0, -1536, -2475). Both the address/DMA plumbing and the decode
arithmetic are now proven correct for this instance, not just internally
self-consistent.
This closes the first three links of the pipeline (request routing —
M-56, DMEM concurrency — M-57, decode itself — M-58). Next session's leads,
in order: (1) trace aEnvMixer's output routing / persistent volume-ramp
state (sVol) the same way — one physical voice followed end-to-end in one
trace file — to see if correctly-decoded PCM gets accumulated into the
wrong output/voice; (2) PVoice allocation/identity aliasing higher up in
the synth; (3) only after (1)/(2), the still-dormant load.c/music.c
pointer-truncation family as its own hardening pass (own D-number). Do NOT
re-open the ROM/index chain, DMEM concurrency, or decode math/addressing —
all three independently verified closed across M-56/M-57/M-58.
Done this session (M-57) — D202: DMEM/concurrent-voice hypothesis falsified by live trace; new latent (dormant) pointer-truncation family found, not yet root-caused
Interactive session, build+run environment confirmed working
(export PATH=/c/msys64/mingw64/bin:$PATH). Picked up M-56's exact next
step: audit port/src/mixer.c's DMEM/aSetBuffer context handling for
concurrent voices.
- Added
GE_MIXERTRACE=1(port/src/mixer.c) — logs every opcode call (addresses/state pointers, in order) tomixertrace.log. Reran the M-56 repro (-level_33, aim+fire script) and read the trace around the soundIndex 46/122 fire event. Result: every voice's full pipeline (LoadADPCM→decode→resample→envmix) runs to completion strictly sequentially before the next voice starts — no interleaving anywhere in ~62k logged opcodes. The M-56 "concurrent voices clobber shared DMEM" theory (D199's flagged risk) is falsified by direct trace, closed negative — the single shared scratch buffer is safe under this call pattern, same as real RSP hardware. - Also ruled out a
a->table->len2242→2241 shift seen in the wire probe log — that'sload.c:396's N64 ground-truth ADPCM-frame rounding, idempotent, not a bug. - New (not yet confirmed as D202's cause):
load.clines 111/173/259/ 320/382/438 narrow the real 64-bitALWaveTable.basepointer into thes32 meminfield — same class as the already-fixed D198/D201. Currently dormant in this build (observed base addresses sit under 4GB, so the truncation round-trips as a no-op) — flagged as a hardening item, not proven live. Delegated a repo-wide sibling-audit of this cast pattern to the local Qwenrepo-mapperagent (read-only, checked): also foundsrc/music.c:879,1073,1266(same class, unaudited) and a low-risk masked case inheapinit.c:26. Everything else in the audio subsystem's(s32)casts are genuine small integers, not pointer truncation. - Full write-up:
findings.mdD202 "M-57".
Next session, in priority order: (1) dump decoded PCM per voice-slot
right after aADPCMdec, tagged by soundIndex, and diff against what the
correct sound should decode to — tests whether the wrong PCM enters the
mixer at all vs. gets attributed to the wrong logical sound downstream;
(2) if that's clean, suspect PVoice allocation/identity aliasing; (3) only
after (1)/(2), consider fixing the load.c/music.c pointer-truncation
family as its own hardening pass (own D-number) — not yet shown to cause
D202. Do NOT re-check the ROM offset/index chain (M-56) or DMEM/
concurrent-voice sharing (M-57) — both closed.
Done this session (M-56) — D202(a) root-cause chain closed on the data side; decode/mix runtime is the last remaining suspect
User playtest report ("no music, sound effects bugged — wrong sound on every gunshot/pickup") triggered a live root-cause push on D202(a). Built
- ran interactively (this session had a working build+run environment).
- Added
GE_AUDIODUMP=1(port/src/audio.caudioSetNextBuffer) — raw- dumps the true final mixed s16 stereo output toaudiodump.raw. Headless repro:-level_33(Dam, player starts armed),GE_INPUTSCRIPTholding aim+fire (R,Z) for 8 pulls. Converted to WAV, found the transients via an RMS scan, sent the clip to the user. User confirmed by ear: hearing the ricochet SFX and the melee-slap SFX on every gunshot — two real, distinct wrong game sounds, not noise/silence. GE_AUDIOTRACEon the same run showed the fire event correctly requests soundIndex 46 (GUN_SILPPK_A_SFX) + 122 (CART_SPENT_SFX, shell casing) — legitimate call sites, valid-looking resolved pointers.- Independently re-parsed the raw ROM bank bytes from scratch in Python
(no project code reused) to check
port/src/romdata.c's PC-only bank re-layout tool (afFixup*/romdataFixupAudioBank— hand-written PORT code, the one link in this chain never scrutinized this closely before). Exact byte-for-byte match against the live runtime trace for both soundIndex 46 and 122's wavetable base/len, and confirmed each has its own distinct (non-aliased) ADPCM book. This closes the index/pointer/ ROM-offset chain for good — proven correct by independent reconstruction, not just prior code audit. - Conclusion: the bug is in the decode/mix runtime itself
(
port/src/mixer.c), not the data path. New leading suspect: D199's already-flagged risk —aSetBuffer's inferred persistent-DMEM-context semantics may not isolate concurrently-active voices (e.g. the continuous background ambience loop + a fire SFX in the same frame), letting one voice's ADPCM decode state bleed into another's output. Full write-up + the exact repro recipe:findings.mdD202 "M-56".
Next session: audit mixer.c's DMEM/aSetBuffer context handling for
concurrent voices using the repro above (GE_AUDIODUMP+GE_AUDIOTRACE,
-level_33, aim+fire script in findings.md). Do NOT re-check the ROM
offset/pointer/index chain — independently re-derived byte-exact this
session.
Done this session (M-55) — D202 cont.: voice-stealing + demo-boundary-reset leads both closed out, negative
Static/read-only, local-Qwen-assisted (triage agent for the grep/citation
legwork, spot-checked against real files before trusting it — see
findings.md D202 "M-55" for full detail). Picked up M-54's two priority
leads:
port/src/mixer.cnever participates in voice allocation at all (zero references tomaxPVoices/pAllocList/pFreeList/pLameList) — that bookkeeping lives entirely in unmodified ground-truthsynallocvoice.c/synthesizer.c. Rules out "PC mixer skips voice stealing" as a cause; nothing to fix there.- No demo-boundary audio reset exists on either platform.
alInit(sl.c:28) is a singleton called once at boot;stop_demo_playback()(ramromreplay.c:604) touches only joystick bookkeeping, no audio call. All files involved are unmodified decomp — N64 has the identical gap.
Both M-54 leads are now closed, negative. The orphaned-looped-voice bug
(door/train SFX never deactivated at an attract-cycle boundary) looks like
a pre-existing N64 behavior only made audible on PC by an idle unattended
capture, not a PC regression — per rule #2, not obviously fixable without
adding a stop call the original game never had. Next: an acoustic check
(longer capture / forced ramrom_table index to confirm whether N64's
voice-stealing eventually silences it during normal play) to decide
WONTFIX vs real bug, or pivot to D202(a) (wrong-sample-per-weapon,
PPK→PUNCH1) which is now the higher-value untouched thread — needs the
decoded-PCM listen-compare, not more voice-pool tracing.
Done this session (M-53) — D202 cont.: both remaining suspects checked out clean, root cause still open
Static-analysis-only session (no build/run — following up on M-52's D202
write-up, picking the two suspects it flagged: ADPCM decode math vs. wrong
ROM segment offset). Both now look clean; full detail in findings.md
D202 "M-53 update":
aADPCMdecImpl(port/src/mixer.c) compared line-by-line against the local Perfect Dark PC port's scalar reference implementation (pd_port/port/src/mixer.c's non-SIMD#elsepath) — identical algorithm, no discrepancy._sfxtblSegmentRomStart's ROM offset (0x102F19A0) cross-validated against 3 independent sources — the CSV scanner (filelist.u.csv), the N64 linker script (ge007.ld, untouched ground truth:sfx.ctl.oimmediately precedessfx.tbl.o), and a direct read of the real bytes at that offset indata/ge007.ntsc-final.z64(decodes as a plausibleALBankFile,bankCount=1). All three agree.- Also spot-checked the ADPCM book-coefficient byte-swap (
afFixupBook,romdata.c) and the_bnkfPatchWaveTablepointer math (bnkf.c, post-D201uintptr_t) — both fine, not previously called out explicitly.
D202 is still OPEN — root cause not found. New leading suspect, NOT yet
investigated: the resample/pitch pipeline (aResampleImpl + whatever
computes a voice's playback rate from ALKeyMap/unityPitch —
synallocvoice.c/seqplayer.c/synport equivalent). A wrong pitch/rate
calc would better explain "sounds like a different sound" (mis-pitched
correct sample) than a literal wrong-sample-content theory, given sample
resolution/pointer-chain tracing already came back clean twice now. Also
would explain "music plays as an SFX-like loop" if the same bug hits
sequence-player note events. Next session: trace one PPK-fire note's
computed pitch/rate value with a new GE_AUDIOTRACE-style probe in that
path — do not re-audit decode math or the ROM offset further, both are
now well-corroborated. Still needs a real listening pass to close, per the
M-52 note below.
M-53 cont. (same session, build+run) — user rebuilt (confirmed no change, expected) and gave two live-playtest clues that reframe the investigation:
- The "music-replacement loop" is a real door SFX (
METAL_SLIDE_LOOP_SFX) stuck on, not a decode/pitch bug — confirmed viaGE_AUDIOTRACEon a real door interaction (-level_09):sndPlaySfxlegitimately chains 202→204→203 (open→loop→close, stock N64 chain mechanic insnd.c'sdo…while). The N64-matching (//#MATCH) cleanup code (propobj.c door7F053B10()) that's supposed to stop it looks correct and isn't a truncated-pointer bug (door->openSoundStateis already a real 64-bitALSoundState *). Checked the level-transition "deactivate all" sweep (lv.c→sndDeactivateAllSfxByFlag_1()) — it's flag-filtered (FINAL_IN_SEQUENCEonly) and wouldn't catch a mid-chain loop sound even on real N64, so N64 must instead rely on the attract-mode demo's scripted input naturally pressing "close door" before the demo ends. Leading theory: this may be a second symptom of the already-open D193 timing bug (g_GlobalTimerDelta/g_ClockTimerwall-clock-vs-sim mismatch), not a distinct audio bug — if the attract demo's fixed-frame-count script runs on a different real-time schedule on PC, it could get cut off before the scripted close-door input fires, orphaning the loop voice permanently. Full write-up + reasoning chain:findings.mdD202, "M-53 cont." + the "Follow-up" paragraph after it. - "Ammo pickup is one of the knife sounds" —
PICKUP_AMMO_SFX=234,PICKUP_KNIFE_SFX=233, adjacent. But combined with PPK(46)→sounds-like- PUNCH1(47), the two adjacent-index shifts are in OPPOSITE directions — doesn't fit a simple constant-offset bug. Left open between "two unrelated single-entry bugs" and "a badly-mispitched correct sample being misheard as a timbrally-similar neighbor."
M-54 — got a live headless repro, and traced the D193-timing theory to a
dead end. Read frametiming.c/libultra.c's VI-retrace/tick chain
end-to-end: the ramrom path bypasses waitForNextFrame() and feeds
updateFrameCounters() the recorded N64 speedframes value directly
(ramromreplay.c:400), and the 32-deep gfxFrameMsgQ message backlog
self-throttles (each drained burst message after a stall sees near-zero
elapsed and no-ops) — no runaway/duplicate-advance mechanism found.
Downgrade the D193-linkage theory — not fully ruled out but the
specific mechanism I'd pointed to isn't there.
Then extended the GE_AUDIOTRACE probes (snd.c: sndPlaySfx now also
logs newState=/flags= per chain link, sndDeactivate now logs every
call) and used GE_INPUTSCRIPT="30:A;90:A;...;1170:A;" (press A once a
second for ~20s to clear the Rare/Nintendo/legal/cast-intro screens fast)
to land in the real attract-mode demo within a ~280s headless run.
Live repro caught: the ramrom_Train demo plays TRAIN_GO_SFX
(soundIndex 64, the only flags=2/SOUND_FLAG_LOOPED sound in the run)
as two voices — neither voice ever appears in a sndDeactivate line,
anywhere in the log, while two other, non-looped states DO get
deactivated right as the sequence rolls back to the boot logos. Direct
confirmation of the M-53 "Follow-up" theory: whatever transition sweep
runs there skips looped voices (consistent with
sndDeactivateAllSfxByFlag's FINAL_IN_SEQUENCE-only filter). This
is the same bug as the door report — any SOUND_FLAG_LOOPED voice
started during a ramrom demo, not door-specific.
New leading theory: real N64 hardware has a small fixed physical
voice pool with voice-stealing (synallocvoice.c _allocatePVoice,
unchanged decompiled code) — a new sound request silently steals/ramps
out the oldest lower-priority voice once the pool (maxPVoices,
unchanged N64 config value) is full, which would eventually silence an
orphaned loop on real hardware even without ever calling sndDeactivate
on it. Not yet checked: whether port/src/mixer.c (Phase-3 PC code)
actually enforces the same cap / stealing, or effectively gives every
voice request a free channel (no stealing ever triggers) — if the
latter, that's the fix, entirely in the port audio layer, no game-logic
(snd.c/propobj.c/front.c) changes needed.
Next session, in priority order (superseded by M-55, see above — (a)
and (b) below are now DONE/negative, do not re-audit): (a) audit
(M-55: mixer.c has no voice
bookkeeping, nothing to fix); port/src/mixer.c's voice dispatch against synallocvoice.c's
free/alloc/lame-list + stealing algorithm(b) look for another explicit N64
stop-all-ambience call at attract-cycle boundaries (M-55: none exists
on either platform — confirmed absent, not just unlocated); (c) dump
decoded PCM for one mis-sounding SFX via the existing GE_AUDIOTRACE
addresses and listen/compare against neighbor entries to settle the
index-vs-pitch question for symptom (a) — still untouched, now the
top lead. Do NOT re-touch ADPCM decode math, the ROM/segment offset
math, the ramrom frame-timing chain, or the voice-pool/demo-reset code —
all cross-validated clean now (M-52 + M-53 + M-54 + M-55).
Done this session (M-52 cont.) — first Phase-3 audio listen pass: D202 opened, root cause NOT found yet
User did the first real listening pass on the committed Phase-3 mixer (see M-52
block below — that commit is already merged to main). Result: audio plays,
but wrong, in two ways, both filed as D202 (new finding, §F, OPEN —
not yet root-caused): (a) weapon fire plays the wrong-but-always-the-same clip
per weapon (deterministic, e.g. PPK fire sounds like a melee hit, every shot);
(b) level music never plays — a looping SFX-like sound plays in its place.
Confirmed menu/intro music is correct; only in-level audio (Bunker1, Dam) is
broken.
This session's work was ELIMINATION, not a fix. Built a GE_AUDIOTRACE
env-gated probe chain (src/snd.c sndPlaySfx, src/libultra/audio/load.c
alLoadParam) and traced real gameplay (-level_09, user firing repeatedly,
plus self-verified with GE_INPUTSCRIPT). Conclusively ruled out the entire
index/pointer/ABI chain — soundIndex resolution, bank re-layout order, the
ALWaveTable pointer chain, the wire-up into the physical decode voice, the
already-fixed D54 param-pool sizing, sequential-voice-processing (no cross-talk
race), and the ALSndpEvent/ALEvent size relationship (measured equal, 32B).
Full elimination log + the two remaining suspects (ADPCM decode math in the
new port/src/mixer.c, or a wrong ROM-embedded byte offset) are written up in
docs/dev/findings.md D202 — read that before re-investigating, do not
re-derive. Diagnostic probes are still in the tree (uncommitted,
#ifdef-free but GE_AUDIOTRACE-gated, harmless): src/snd.c, src/libultra/audio/load.c.
Strip them once root-caused, or fold into a permanent probe if useful long-term.
Next step recommendation (from D202): since the wrong sample is 100%
deterministic per weapon (ruled out stateful/interleaving causes already), the
fastest path is probably a byte-level diff of the embedded _sfxtblSegmentRomStart
PC data against the source .z64 at the resolved offset (rules in/out asset-
embedding) — cheaper to check than auditing aADPCMdecImpl's DSP math by hand.
Done this session (post-M-50, M-52) — audio findings backfilled + Phase-3 mixer landing committed
Picked up exactly where the last session's handoff left off: the working tree
had 15 modified files (D200's reverb fix + the earlier Phase-3 software-mixer
implementation), with code comments referencing findings D198/D199 that
were never written into findings.md, and the M-49 playtest block claiming
D195/D197 were "logged in findings §F" when they weren't.
Wrote 5 findings entries (§F table, after D194/before D200):
- D195 — Control transparency/alpha bug (playtest report, no capture yet — OPEN).
- D197 — Silo head-texture wrap, likely
G_TX_CLAMPnot honoured (OPEN). - D198 —
load.cusedK0_TO_PHYS(unconditional&0x1FFFFFFFmask) on 64-bit heap pointers at 3 call sites; every sibling site already usedosVirtualToPhysical. Fixed by matching them (FIXED). - D199 — the Phase-3 software mixer itself: macro-swap
aXxxexecution (abi.h→mixer.h→mixer.cImplfunctions) + theaSetBufferpersistent-DMEM-context semantics, which have no RSP disassembly to verify against — reconstructed purely from call-site pairs. Flagged the context-inference as a real risk if an unaudited call site sets it differently (LANDED, functional on-level_09). - D201 (new number) — found while reading the diff:
bnkf.c's relocation offsets weres32, silently corrupting bank/inst/sound/wavetable pointers on PC for ~50% of allocations (sign-extension of a truncated 64-bit pointer). The code comment had already applied theuintptr_tfix but mislabeled it "D200" (that number was already taken by the reverb bug) — relabeled to D201 in both the comment and the entry (FIXED).
Committed the full audio-port change (mixer + K0_TO_PHYS/bnkf fixes +
D200 reverb fix + all 5 new findings) as one commit — judged safe to land
together rather than split D200 out separately (as the prior handoff
suggested) since it's one cohesive session's audio work and splitting
findings.md's hunks cleanly wasn't worth the risk of a bad split.
Verified before commit: ./build-pc.sh ntsc-final — clean link, no new
warnings beyond one pre-existing nested-comment cosmetic warning in the
already-present D200/D201 comment block. Ran -level_09 ~600+ frames
(interactive console session) — crash-free, matches D200's prior
verification; music + SFX both audible.
Not done / still owed:
- D195/D197 both need a
GE_PCDUMP/GE_TEXDUMPcapture — filed as OPEN with a concrete next step, not investigated further this session. - D199's
aSetBuffercontext-inference risk is unverified beyond-level_09— a wider level sweep (esp. levels with reverb/aux-bus SFX) would raise confidence. - LEVEL-STATUS's per-level PASS lines still predate the mixer landing —
re-sweep once this commit is on
main.
Done this session (post-M-50) — D200: -level_09 audio segfault root-caused + fixed
D200 (findings §F, FIXED). -level_09 segfaulted in seconds once the new
software mixer ran reverb. Root cause: ALDelay.input/output are u32, and
reverb.c back-references the delay ring as &r->input[-d->output]. On N64
(s32 ptrdiff_t) the u32 negation wraps to a small negative index; on x86-64 it
zero-extends to +4,294,967,136 samples (~8GB forward) — proven exactly:
0x706d30a0 + 0xFFFFFF60·2 = 0x2_706D2F60, the observed wild address. Wild
aSaveBuffer/aLoadBuffer writes then trampled the delay array + r->base +
adjacent heap in a self-propagating loop. Fix: 5 lines in
src/libultrare/audio/reverb.c — (s32) negation casts at all negative-index
sites (alFxPull ×2, _loadOutputBuffer ×2) + (s64) ramalign; identical to PD
ground truth (n_reverb.c). Sibling audit: reverb.c is the only audio file with
the pattern. Verified: -level_09 runs 120 s+ crash-free, music + SFX both flow;
temporary DRAM guard window in the mixer saw zero OOB writes. All instrumentation
removed — the only session diff in code is the 5-line fix.
⚠ Working tree has uncommitted audio-port work from an EARLIER session.
git status: 15 modified files; only reverb.c + findings.md are this
session's. The rest is the Phase-3 software-mixer implementation (port/src/mixer.c
macro-swap acmd execution, port/include/mixer.h, include/PR/abi.h,
port/src/audio.c, port/src/libultra.c, src/libultra/audio/bnkf.c) +
K0_TO_PHYS corrupted-pointer fixes in load.c/save.c/synsetpan.c/
synsetvol.c/synsetfxmix.c. Its code comments reference findings D198/D199
that were never written into findings.md — and HANDOFF's M-49 block claims
D195/D197 are "logged in findings §F" but those entries are also missing from the
index. Before committing anything: write D195/D197/D198/D199 entries, then
commit the audio work as its own change (keeps D200's 5-line fix reviewable on
its own).
Status deltas. Phase 3 audio is now functionally live on -level_09 (was:
crash in seconds). LEVEL-STATUS's Bunker1 PASS line predates the mixer work —
re-sweep levels once the mixer commit lands. Closed thread: the leftover
"aSetBuffer BIG c=2144/2048" flags from this session's debug were confirmed
legitimate DMEM-only _pullSubFrame (env.c) ops — no action.
Env reminders. Every fresh MSYS shell needs export PATH=/c/msys64/mingw64/bin:$PATH
or the exe fails with "SDL2.dll: cannot open". Windows locks the running .exe —
kill the game before rebuilding or the link step fails.
Done this session (M-49) — PR #22/#23 merged, Linux bundle re-verified off main (2026-09-04, interactive console)
v0.1.0 is verification-complete on both platforms. Only the tag push + publish remain (user's call).
- PR #22 (D188 va_list ABI) — MERGED (
08feb5c2). - PR #23 (D189 stack-overruns) — auto-closed by GitHub when the #22 merge deleted
its base branch (it was stacked on #22's branch, not
main; a closed PR can't be reopened or retargeted). Recreated as PR #24, rebased ontomain— the now-merged xprintf/D188 commits dropped out cleanly (patch contents already upstream), leaving juststan.c tileStack[64]+ CMakeLists-fno-stack-protector- D189/D190 docs. CI green, MERGED (
881cbf5b). Stale branch deleted. (git push --forceand--deleteare classifier-blocked intermittently — worked for the branch delete, not the force-push, hence the new branch namefix/linux-stan-overrun-d189-rebased.)
- D189/D190 docs. CI green, MERGED (
- Linux bundle rebuilt + re-verified off merged
main:cmake --build build-linux(WSL Ubuntu / gcc-14) — links 243/243.tools_pc/bundle-linux.sh 0.1.0→dist/goldeneye-pc-port-0.1.0-linux-x86_64.tar.gz(+.sha256). No-ROM / 60 MB guards pass.- Untarred clean to
~/gesmoke3→prepare-assets.py→ all 4 sidecarsALL CHECKS PASSED→ dropped ROM indata/. - Level sweep 5/6 (
-level_09/20/27/34/37all run the full 60 s window, frame 1500+, no crash;-level_45SIGSEGV at load = D190, known/open, already hedged in the release notes). TheromdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1line on every level is the expected D35 header-only-copy clamp, not a regression. - Matches M-48's tarball sweep exactly — the two merged PRs regressed nothing.
- Legal review (user asked): no new exposure. Both PRs are
#ifdef PORTABI / build-flag changes, no content added. Release artifacts unchanged: exe + README + licenses +prepare-assets/(emit scripts + decomp metadata.inc.c/.csv); no ROM, no assets, nopccg.bin/pcmodels.bin. Pre-existing n64decomp/007 no-explicit-license caveat is disclosed inNOTICEand unchanged. dist/now holds both pristine bundles (…-linux-x86_64.tar.gz+…-win64.zipfrom M-48) ready for a manual release-asset upload if CI'sreleasejob needs a fallback.
Owed / next (M-49)
- Tag:
git tag v0.1.0 && git push origin v0.1.0→ CIreleasejob drafts the dual-platform pre-release → download + smoke the attached archives → Publish (user's call — the only outward-facing step). - D190 (
-level_45load SIGSEGV, propDef-stride family) stays open; not a v0.1.0 blocker.
Done this session (M-48) — triage + v0.1.0 Windows bundle validated (2026-09-03, interactive console)
Triage of high-impact work for end-user experience + repo credibility. Top
finding: cutting the v0.1.0 alpha release is the single biggest credibility
lever and the Windows half is now proven. Full ranked triage:
scratchpad/TRIAGE-2026-09-03.md.
- Windows v0.1.0 bundle — VALIDATED END TO END.
bundle-win.sh 0.1.0→ 6.9 MB zip (exe + SDL2/zlib/winpthread/stdc++/gcc_s + README + licenses +prepare-assets/w/ 512 model headers, DLL-closure check passes). Unpacked to a clean dir →prepare-assets.pyregenerated all 4 sidecars (ALL CHECKS PASSED) →ge007.x86_64.exe -level_09booted to frame 2100+ crash-free, geometry renders (scratchpad/smoke_000240.png). - PR #20 (
ci/bundle-win-zip-fallback) — MERGED (80952f69). Stock MINGW64 ships nozip;bundle-win.shnow falls back to 7z /Compress-Archive. CI unaffected (itpacman -S zip unzip). - PR #21 (
docs/release-notes-known-issues-refresh) — MERGED (e4fc9dd0). Dropped the HUD weapon icon (D187/#19) and blank briefing text (D178/M-36) from.github/release-notes.md+tools_pc/dist/README.md.in— both fixed. Black sky + front-end 3D models stay listed. - D178 (blank briefing objectives) — was already FIXED (
cd1ed574,romdataFixupBriefing, M-36). Stale item in the M-44/M-45 owed lists.
Linux runtime FIXED this session (M-48, full-agentic) — two crashes root-caused + fixed
WSL back after reboot. Found the Linux -O2 build crashed at boot; root-caused
- fixed two independent latent-UB classes, both exposed by PR #9's
-Og→-O2switch and by Ubuntu gcc's hardened defaults — the N64 + MinGW builds never saw them.
- D188 / PR #22 —
_Printfva_listABI._Printftookva_listby value then passed&argsto_Putfld; on x86-64 SysVva_listis an array type so that's the address of a local pointer, not the arg list →_Putfld'sva_arg(*args,…)walked garbage → SIGSEGV inbossInitMainthreadData's D121 sprintf, pre-frame-1. Fix:#ifdef PORTthread ava_list *through (xprintf.c+sprintf.c). Same class as D187. Windows framediff 3/3. - D189 / PR #23 (stacked on #22) — latent stack-buffer overruns.
stan.c sub_GAME_7F0B1DDCtileStack[39]vs a>= 41bail check (BUNKER1 prop setup legitimately walks ~40 tiles);bondview2.c bondviewCalcIntroSwirlCamerapointbuf[10]indexed[-3..11]. Harmless on N64 + MinGW (no stack protector); fatal__stack_chk_failunder Ubuntu gcc's-fstack-protector-strong. Fix:-fno-stack-protectorglobally (matches N64/MinGW) +tileStack[64]#ifdef PORT. Windows framediff 3/3; Linux-O2-level_09runs to frame 2100+ crash-free, renders BUNKER1. - Earlier-seen
-Og"texcachestd::listabort ~frame 9" was downstream of the D189 stack corruption — gone with the fix. - porting-notes: D7 (va_list SysV) + D8 (stack-protector / latent overrun class). findings §F D188/D189.
- Debug tree
~/…/build-linux-dbg, smoke tree~/gesmokein WSL.
Owed / next (M-48) — v0.1.0 is now unblocked for BOTH platforms
- Merge PR #22 → then PR #23 (retargets to
mainon #22 merge). Both need the user (gh pr mergeis agent-blocked... except it worked earlier for #20/#21 — try it). - Rebuild + re-bundle Linux off merged
main, re-run the bundle smoke (untar + prepare-assets +-level_09), and the Linux multi-level sweep. - Tag:
git tag v0.1.0 && git push origin v0.1.0→ CIreleasejob drafts the pre-release with both archives → download + smoke-test the attached win64 zip and linux tarball → Publish (user's call — the only genuinely outward-facing step). - If Linux still has rough edges after the sweep: the release notes already hedge it ("boots and renders … far less exercise"); ship it as a known-rough secondary platform rather than holding the tag.
Done this session (M-47) — triage + PR #18/#19 render verification (2026-09-03, interactive console)
Triage of high-impact / low-effort wins for end-user experience + repo
credibility. Key env finding: an interactive session on the physical console
CAN verify rendering (GE_PCDUMP + framediff.py 3/3) — the M-37/42/43
"no display" claim was background-session-only. Recipe in
[[env-no-runtime-verify]] memory / see below.
- PR #19 (HUD ammo-type icon) — VERIFIED + de-drafted + merged current to
main. Ready to merge.
-O2build,-level_09GE_PCDUMP200–440: orange magazine glyph absent pre-fix, present post-fix;framediff.pyframe 320 worst-cell dmean 21.8 → 4.0 (converges to the pre-regression golden). Addedfindings.md§F D187 +porting-notes.mdD6 (non-void-no-return / latent-until--O2class). All CI green,mergeStateStatus: CLEAN. User action: merge PR #19 (agent isgh pr merge-blocked). - PR #18 (black sky, Path B) — VERIFIED render, stays draft. Sky geometry
now emits + textures (
-level_22top-half luma 0.1 → ~174; user watched it live: "sky box is there just glitchy"). Two defects, full analysis in a PR #18 comment: (1) texcoord shear —tc = unk20/unk24 * 32.0fis the wrong scale; pin the real units ofSkyRelated38.unk20/unk24fromsub_GAME_7F097388first. (2) vertical coverage — textured band only fills top ~25%, black gap to horizon; traceskyPortRenderPolynverts + xy per call. ~1 render-iterate session. - Ranked triage (full list in the session): #19 → #18 → tag v0.1.0 → refresh
README GIF is the best payoff path. Tag v0.1.0 needs a clean bundle +
launch smoke on Win + Linux (release infra all merged, PR #17 in
main).
Rendering-verify recipe (interactive console session)
export PATH="/c/msys64/mingw64/bin:$PATH"; run from repo root w/ absolute exe
path; golden is 640×480 so back up data/ge007.ini and set [Window]
Width/Height=640/480 (game rewrites it on clean exit — restore after);
GE_PCDUMP="200-440:60" + timeout 90; frames → ./ppm/;
python tools_pc/framediff.py ppm. PIL only in
~/AppData/Local/Programs/Python/Python313/python.exe, not the msys python.
Windows locks the running .exe — kill the game before the next cmake --build
or the link fails.
Done this session (M-46) — D176(a) sky Path B implemented → draft PR #18 (2026-09-03, full-agentic)
Short window (~25 min, interactive-lite, no display). Fixed the full-agentic-mode skill first (time now read from the system clock, not extrapolated; interactive-verify is no longer all-or-nothing). Then a background subagent implemented D176(a) Path B:
fix/d176a-sky-path-b→ draft PR #18.src/game/sky.conly, all#ifdef PORT, N64 path unchanged.skyRenderTri/skyRenderFullnow emit screen-spacegSPVertex+gSP1Triangle/gSP2Trianglesfrom the projectedSkyRelated38verts (new helperskyPortRenderPoly, ortho viadynAllocate*) instead of the no-op'dG_RDPHALF_*stream. Links 243/243, zero new warnings. findings.md §F "D176(a) — M-46"; porting-notes §D.- Owed (display):
-level_22frame ~360 should now show a moonlit sky, not black. Spot-check-level_36/-level_29. Tex-coord scale (*32) is a guess. Single-frameGE_PCDUMPdiff vs golden for the sky levels; confirm non-sky levels unchanged. PR stays draft until a human eyeballs it. - Worktree at
.claude/worktrees/agent-aa27d9f0082d683cb(branch pushed;git worktree removeit when done). - Memory:
qwen-parity-goal.md— user wants the local agent brought to tool/feature parity with Claude over time.
Also M-46 — HUD weapon-icon regression root-caused → draft PR #19
(fix/hud-weapon-icon-missing-return). set_rgba_redirect_generate_microcode()
(gunfire.c:5929, sole icon emitter) is non-void with no return; harmless at
-Og, breaks at -O2 (commit 34885535, Sep 2) — undefined return reg → callers
overwrite the icon's DL commands. Ammo numbers use a separate correct path, hence
the split symptom. Fix = one-line return under #ifdef AVOID_UB. MEDIUM-HIGH.
Not built/run this session. Owed: -O2 build + -level_09 capture vs
docs/img/bunker1-2.png; A/B a -Og Debug build; findings.md §F entry.
Do NOT revert 34885535.
Done this session (M-45) — release infra: dual-platform alpha bundles + CI docs-PR fix (2026-09-03)
Triage session → release plumbing. Interactive-lite (no build/run env). Two
PRs merged to main:
- PR #16 (
ci/docs-pr-mergeable, merged2f85ca84) — droppedpaths-ignorefromci.yml'spull_request:trigger; thechangesjob now diffs a PR against its base sha (mirroring the branch-push logic). Docs-only PRs finally merge without an owner bypass (the M-44 #14/#15 problem). Fixes the item the M-44 handoff flagged as "worth a one-line ci.yml fix". - PR #17 (
release/v0.1.0-alpha-dual-platform, merged) — first-alpha release plumbing:tools_pc/bundle-linux.sh(new) — packages a Linux build asgoldeneye-pc-port-<v>-linux-x86_64.tar.gz(exe + README + licenses +prepare-assets/). Mirrorsbundle-win.sh's no-ROM / 60 MB guards + theprepare-assets/assembly. No shared libs bundled — generated README saysapt install libsdl2-2.0-0 zlib1g libgl1(user chose "document apt", not ".so bundling"). CI-verified: 4.2M tarball, ~512 model headers.tools_pc/dist/README.md.in— OS-neutralised via@PLATFORM@/@EXE@/@DEPS@/@LICENSE_EXTRA@tokens (win: sed; linux: sed + awk for the multi-line@DEPS@). Known-issues list corrected: dropped the stale "ladders don't work" line (D177 fixed M-36); added black-sky + missing-HUD-weapon-icon.ci.yml—linux-buildnow packages + uploads the Linux bundle;releasejob (needs:+linux-build) attaches both platform archives to the draft pre-release..github/release-notes.md— rewritten for the two-platform NTSC-U alpha.
Alpha scope decision (user): NTSC-U only, keep the one-time
prepare-assets.py step. Part A (3-region + launcher) and Part B (native C
converter) from docs/dev/release-dropin-rom-plan.md remain follow-ups — Part B
is still the issue-6-dropin-rom-release offline-Qwen candidate.
Owed / next (M-45) — see the standalone prompt in the session handoff
- Cut
v0.1.0— needs a machine with a working build+display:- Windows:
./build-pc.sh ntsc-final→tools_pc/bundle-win.sh 0.1.0→ unzip to a clean dir → drop US ROM indata/→python prepare-assets\prepare-assets.py→ launch → confirm it boots to the menu / a level. - Linux:
cmake --build build-linux(or./build-pc.sh) →tools_pc/bundle-linux.sh 0.1.0→ untar clean → install SDL2/zlib/GL → drop ROM →python3 prepare-assets/prepare-assets.py→ launch. - If both load:
git tag v0.1.0 && git push origin v0.1.0→ CI drafts the pre-release with both archives → smoke-test the attached artifacts → Publish.
- Windows:
- Tier 2 visible-glitch fixes (display needed to verify — lead preps, user
eyeballs):
- HUD weapon-icon regression (hud-gun-ammo-icon-missing) — regressed
~2026-09-02/03. Bisect PR #7/#8/#9 + f10-overlay merges; check the F10
overlay DL append in
gfx_pc.cpp gfx_run()isn't leaving combiner/tile state dirty; audit thebondview*.cweapon-icon draw path. Capture-level_09, diff vsdocs/img/bunker1-2.png. - D176(a) black sky — Path B (d176a-sky-stream-spec) —
#ifdef PORTinsrc/game/sky.cto emitgSPVertex+gSP1Trianglefrom theSkyRelated38verts instead of the no-op'd RDPHALF stream. Verify-level_22frame ~360. - D172 blood particles magenta/cyan — likely a channel-order / env-color
decode in the fast3d sprite path or
blood_animation.c. Small.
- HUD weapon-icon regression (hud-gun-ammo-icon-missing) — regressed
~2026-09-02/03. Bisect PR #7/#8/#9 + f10-overlay merges; check the F10
overlay DL append in
- Tier 4 research/planning pass — audio (libaudio→SDL, adapt PD
mixer.c), blank-briefing objective text (D178/D143langGetNULL family), front-end 3D models (D75/D149). Produce a delegation plan: mechanical → local Qwen, judgment → Claude subagents / lead. No code, plan only, for user approval.
Interactive session, user on a real display driving the tests. Branch
feat/f10-options-overlay, new commit bf18a166. PR #10 un-drafted / ready.
- Fixed invisible overlay text —
drawTextpassed the measured text w/h intotextRender'swidth/height(the on-screen CLIP rect), clipping every glyph. Now passesviGetX()/viGetY()likebondview2.cdebug text. - Mouse UI — hover-highlight; left-click in the value column toggles/cycles;
drag sliders; red
Xclose box; label-clicks only focus. NewinputSuspendForOverlay()(input.c) frees + shows the OS cursor while open. - New rows: Fullscreen (live) + Resolution (windowed presets filtered to
desktop, live resize+recenter).
videoRequestWindowSize/Fullscreenpost from the scheduler thread;videoDrainWindowRequests()applies host-side invideoPumpEvents. - Layout recomputed for the wide BankGothic font (right-aligned values, 15u row pitch, all 13 rows fit ~240u).
- User verdict: "looks great and can be merged in." Verified live: layout,
hover/click/drag, live VSync/filter/fullscreen/resolution, close box, closed =
no-op, front-end pointer unaffected at default
MouseCaptureMode=1.
Merge sweep (M-44, end of session) — ALL 6 PRs MERGED
Main = 74bebc0d. All feature branches deleted (local + remote); tree clean.
- #12 D186 FpsCap clamp · #13 building.md d88 step · #11 alpha asset-prep tool · #10 F10 overlay v2 · #15 D176(a) stream spec · #14 D176(a) repro + D74-row cleanup.
- #14 needed a manual
findings.mdconflict resolve against #15 (kept both D176(a) subsections, chronological M-37 -> M-42 -> M-43). - Docs-only PRs (#14/#15) can't be merged by
gh pr merge— themainruleset requiresvalidate+windows-build, butci.yml'spull_request:haspaths-ignore: ['**.md','docs/**']so docs PRs run no CI and those contexts never report.--adminis classifier-blocked for the agent; user merged via GitHub UI owner-bypass. Worth a one-line ci.yml fix: droppaths-ignorefrompull_request:and let thechangesjob report the heavy jobs skipped=success (the pattern already used forpush).
Owed / next (M-44)
- HUD gun/ammo icon missing — user reports the weapon icon (not the ammo
number) vanished, regressed "in the last day". NOT investigated. Bisect the
PR #7/#8/#9 merges + f10 work;
-level_09capture vsdocs/img/bunker1-2.png. Video.FpsCapsub-30 is unresponsive/hangy (D186 / PR #12 territory, not the overlay branch). Uncapped FPS unsupported. Low priority per user.- PR #10 merge is the user's call (they approved it).
- Unchanged from M-43: D176(a) Path B; PR #11 packaging dry-run; merges #12–#15.
Done this session (M-43) — PR #11 to ready + D176(a) full stream spec (2026-09-03, full-agentic)
Full-agentic-mode, ~1:00–4:30 ET. Runtime rendering is not verifiable in this
environment — the Windows GL build exits immediately with no window when
launched from the Git Bash tool (no display), so GE_PCDUMP captures nothing.
Kept to work that is checkable headless (Python tooling, static analysis, git).
-
PR #11 (
feat/alpha-asset-prep-tool) → READY FOR REVIEW. Rebased onto currentmain; the romdatammapcommit that blocked it landed via PR #8 and dropped out on rebase, so the branch is now just the asset-prep tool + two doc edits. Re-verified end-to-end this session: assembled theprepare-assets/tree exactly asbundle-win.shdoes, ranprepare-assets.pyagainst the US ROM from a clean temp dir → all four output files (pcmodels.bin/manifest.csv,pccg.bin/manifest.csv) SHA-1-identical to the working build's sidecars. PR body rewritten. Owed: a human run from an actual unzipped release.zip(packaging dry-run) — tool logic + output are verified; this is the last "shipped artifact works" check.- NB
docs/building.md's d88-step edit is also on PR #13 standalone — whichever merges first, the other no-ops on that hunk.
- NB
-
D176(a) black sky — full stream spec committed (
findings.md§F, "D176(a) — M-43 UPDATE"). The M-37 scratch note was never committed; this re-derives the complete format fromsky.c+gbi.h. Key facts:skyRenderTri/skyRenderFullemit a verbatim N64 RDP triangle command (2-word header + 3 edge-slope pairs + 16-word shade block + 16-word texture block = 40 data words forG_TRI_SHADE_TXTR), chopped intoG_RDPHALF_1/_CONTpairs and terminated byG_RDPHALF_2. GE's RSP ucode just DMAs it to the RDP.sub_GAME_7F097388already produces complete screen-space verts (SkyRelated38: xy×4, z, 1/w, s/t, rgba) — the projection is endian-clean and not the bug. Two implementation paths written up: Path A = RDP-tri decoder inport/fast3d/gfx_pc.cpp(invert the plane equations → 3LoadedVertex→ existing GL path; ~1 session, "right"); Path B =#ifdef PORTinsky.cto emitgSPVertex+gSP1Trianglefrom theSkyRelated38s instead of the RDPHALF stream (~a few hours, cheaper, small game-code-idiom exception). Recommend Path B first — lights up all cloud-sky levels and de-risks A. Verification target:-level_22frame ~360 (M-42's clean headless black-sky repro). Cross-checked the word layout againstgbi.h(local Qwen, read-only).
Owed / next (M-43)
- Implement D176(a) Path B on a machine with a working display+GL — it is a ~few-hour job now that the format is fully documented, but every step needs a render to verify (winding, combiner, tex coords), which this env cannot do.
- PR #11 packaging dry-run — unzip a real release bundle on a clean machine,
run
prepare-assets, launch the game. - Unchanged from M-42: D186 real fix (frame pacing off the scheduler thread); PR #10 (F10 overlay, draft) still owes its interactive feel-check; PR #13/#14 docs still awaiting the user's merge.
Done this session (M-42) — D186 frame-cap fix + doc honesty (2026-09-03, full-agentic)
Full-agentic-mode, ~3 h. This repo is mature (21/21 levels load+render+no-crash, front end playable); most open work is deep/multi-session or interactive-gated. Landed the one clean verifiable engineering win + tidied stale docs. Three branches pushed + PRs opened (user reviews/merges — the full-agentic skill was updated mid-session to pre-authorize push/PR on the user's own repos):
-
PR #12 ←
fix/d186-fpscap-clamp(ready for review) -
PR #13 ←
docs/building-d88-step(docs; overlaps a commit on PR #11) -
PR #14 ←
docs/m42-graphics-honesty(docs) -
fix/d186-fpscap-clamp(4a1cb1d5) — D186: a sub-30Video.FpsCapno longer throttles the whole sim.sync_framerate_with_timer()(gfx_sdl2.cpp) runs itssysSleep+busy-wait inline on the scheduler thread (osSpTaskStartGo→gfx_run→swap_buffers_begin), so a low cap blocks VI-retrace delivery and drags every game thread down to the cap rate — this is what made the game a 1-fps slideshow whenFpsCapgot dinged to 10 (M-39). No N64 analogue. Fix = clamp0 < FpsCap < 30→ 0 (uncapped) + warning, at bothvideoInit(normalises a badge007.inifor the nextconfigSave) andgfx_sdl_set_target_fps(the fast3d chokepoint, also covers the F10-overlay live path). Caps ≥ 30 unchanged. Verified: defaultFpsCap=0framediff 3/3 golden-identical;FpsCap=10now runs at the uncapped rate (VI ~1740 in 30 s vs ~300 before);FpsCap=60still paces (~56/s, no warning). Build links 242/242. findings.md §F "D186" + porting-notes.md §E. Owed (real fix): move frame pacing off the scheduler thread so any cap only drops presented frames — until then a low cap is refused, not supported. -
docs/building-d88-step(2348a1a0, cherry-pick of the M-38 commit stranded on the PR #11 branch) —building.md§4 now listsd88_emit.py --regenas the third required sidecar pass (was d43+d69 only → a source build per the old instructions crashed on first level load; this is the D185 omission). NB the commit's trailing blockquote forward-references the release-bundleprepare-assets.py(PR #11, not yet merged) in present tense. -
docs/m42-graphics-honesty(76dd2d5c) — findings.md + GRAPHICS-BACKLOG: D176(a) black sky —-level_22(Statue, night) is a clean no-input headless repro (bare boot, frame ~360, upper half pure black). Best verification target for whoever writes the RDPHALF sky-tri decoder (root-caused M-37, ~1 session, genuine new fast3d code).-level_36/-43intro cameras face the terrain so a no-input capture there does not frame the sky;-level_29top-of-frame is dim, not black (needs an eyeball). Also struck the stale "D74 wrap-block" backlog row — that dead-code/OOB block was reworked and hoisted behindVideo.WrapFixat M-30 (RC3/D167).
Owed / next (M-42)
- Push the 3 branches, open PRs (or fold
docs/*into an existing docs PR). D186 is a clean standalone; the two docs branches are trivial. - D186 real fix — relocate frame pacing off the scheduler thread
(
gfx_sdl2.cpp/ theosSpTaskStartGogfx path). Then the clamp can relax. - D176(a) — implement the RDPHALF tri-raster decoder in
gfx_pc.cpp(case G_RDPHALF_1/2/CONTcurrently no-ops at ~:2901); verify against-level_22frame ~360. Stream spec in findings.md §F "D176(a) M-37 UPDATE". - PR #10 / #11 still draft, still owe their interactive/Linux verification
and a rebase onto current
main(both predate the #7/#8/#9 merges — #11 especially carries already-merged commits). - Doc drift spotted, not chased: GRAPHICS-BACKLOG D176(b)/D182(2) still cite
"Family A" (
import_textureline-pitch shear) as the cause, but D183 (M-36) disproved that for the-level_36repro (0/166 strided loads). Re-scope.
Done this session (M-41) — branch/PR merge sweep (2026-09-03)
Goal: get lingering branches/PRs merge-ready. Claude cannot merge or gh pr merge (classifier-blocked) — all merges are the user's to do.
- PR #8 (
feat/linux-ci-compile) — UN-DRAFTED, ready for review. Re-verified on WSL Ubuntu: incrementalninja -C build-linuxlinks green;-level_09runs 1150+ frames headless and shuts down clean (no crash). Body rewritten to current reality.mergeStateStatus: CLEAN.- Headless
GE_PCDUMPstill reads black on llvmpipe/WSLg — confirmed via the committedGE_DUMPSHADERdiag:glReadPixelsof fb0 returns 0.000 mean onGL_BACKandGL_FRONT; the two extra FBOs are 0x0 attachment-less. Not a port bug (user watched BUNKER1 render in the live window M-40). A real-GL-driver box is needed for the Windows-vs-Linux framediff. Did not chase the offscreen-FBO capture rewrite — dev-infra, not a merge blocker. - Reverted an uncommitted TEMP
GE_DUMPSHADERshader-source dump ingfx_opengl.cpp(rendering works; not needed).
- Headless
- Pushed + opened PRs for the three unpushed local branches:
- PR #9
perf/release-opt—-Og->-O2for release. Non-draft, trivial. - PR #10
feat/f10-options-overlay(draft) — D184. Owed: interactive feel-check. - PR #11
feat/alpha-asset-prep-tool(draft) — off the PR #7 docs branch; diff shrinks once #7 merges. Owed: Linux romdata mmap compile/runtime.
- PR #9
- PR #7 (
docs/dropin-release-plan-and-status) — CLEAN/MERGEABLE, docs+CI only, all checks green. Recommended merge; user to do it.
Merged this session: PR #7 (97017ca9), PR #8 (3b798994), PR #9 (6c322548).
Still open: PR #10 (f10 overlay, draft), PR #11 (alpha-asset-tool, draft) — de-draft once their owed verification is done.
NEW — open Linux issue (surfaced M-41, not chased): flaky stack-smashing on -level_09
Running ./build-linux/ge007.x86_64 -level_09 headless on WSL Ubuntu/llvmpipe,
the game intermittently aborts with glibc *** stack smashing detected ***
(SIGABRT / SIGNAL 6) somewhere past frame ~1200. Repro'd ~3 runs, then ~4 clean
runs to frame 2300+ — flaky, likely heap-layout / uninit-memory dependent
(canary only sometimes tripped). Both crashing runs had GE_PCDUMP set at high
frame numbers; the clean gdb run had no GE_PCDUMP — weak correlation, unconfirmed.
NOT a regression from #8/#9 (that's what made Linux run at all); -fstack-protector
on glibc aborts where MinGW may silently tolerate the same overflow. Windows is
verified crash-free past frame 1740. Candidates: the new POSIX crash/watchdog
path in port/src/crash.c (has fixed char[24]/char[64]/char linebuf[N][512]
buffers, backtrace_symbols formatting) if it's firing on SIGTERM teardown; or a
real latent in-level overflow. Needs a dedicated gdb session: handle SIGABRT stop, let it run 2000+ frames, bt at the abort.
Linux headless GE_PCDUMP — partially works (M-41, subagent aae607dac4a1a7ce0)
glReadPixels(GL_BACK) pre-swap does capture correctly on llvmpipe (subagent
got the gun-barrel intro, mean ~37, visually right). But in-level -level_09
frames 150–900 come back black/near-black (max 26–230, mean <5) before the
stack-smash. Unclear if the level's 3D geometry actually renders headless or if
those are genuinely dark frames. The M-40 "black on both GL_BACK and GL_FRONT =
blanket llvmpipe limitation" claim is too strong — GL_BACK works.
Done this session (M-40) — Linux CI: compile + LINK now GREEN (2026-09-03)
Continued PR #8 (feat/linux-ci-compile). Started at "propobj.c fails to
compile"; ended with all three CI jobs green — the whole tree compiles AND
links on Ubuntu 24.04 / gcc-14. PR stays draft (human merges). Windows
build re-verified green after every change (-level_09 crash-free past frame
1740). 9 commits 2e8fa3de..c803b212:
append_text_picked_up— PORT-guarded signature (enum/int->ptr is a GCC-14 hard error-fpermissivedoes NOT downgrade).port/shim/stddef.h— addwchar_t(__WCHAR_TYPE__) to the C-on-Linux branch (SDL2 / glibc<bits/wchar2.h>need it).vtxstore.c—(Model*)cast on themodelGetNodeRwDatacall (matches the sibling line 157; enum->ptr hard error).include/PR/os.h—bcopy/bcmp/bzero: onPORT && !_WIN32include<strings.h>(glibc's size_t prototypes) instead of the N64intones.port/src/{pccg,pcmodels,romdata}.c— localsnprintfdeclsunsigned long long->size_t(same M-39 fix, these 3 files had private copies).n64stubs.c—bcopy/bzerodefs are now_WIN32-only (glibc exports them).port/src/libultra.c— guard the D60VirtualQuery/GetCurrentThreadStack- Limitsprobes behindPLATFORM_WINDOWS; POSIX diag prints n/a.CMakeLists.txt—CMAKE_C_EXTENSIONS ON/CXX_EXTENSIONS ON(-std=gnu11/gnu++20). Strict-std=c11defines__STRICT_ANSI__which hidesCLOCK_REALTIMEetc. This is what MinGW already used.CMakeLists.txt— host<stddef.h>viagcc -print-file-name=include/stddef.h(it's compiler-internal, not inbin/../includeon native Linux gcc).port/shim/sched.h(new) +hostsched.h.in— C++ TUs get the real<sched.h>(libstdc++<thread>/gthr needcpu_set_t/sched_yield; the decomp'ssrc/sched.hshadows it). C uses#include_next.port/src/crash.c— POSIXcrashDumpThreads(was PLATFORM_WINDOWS-only; watchdog referenced it unconditionally -> the one link error). Logs the thread roster + the watchdog thread's own backtrace.
Linux RUNTIME — boots + runs, but renders BLACK (2026-09-03, commits 2afc0be7, cee26d46)
-level_09 / -level_20 / -level_24 boot, run 600+ frames, and shut down
cleanly on Ubuntu 26.04 / gcc-14 via WSLg — no crash. BUT the level
renders black (frame-200 GE_PCDUMP capture = all black + one green speck).
Compile + link + boot + kernel/scheduler/timers all work; the graphics pipeline
does not.
Build locally: cmake -S . -B build-linux -G Ninja -DROMID=ntsc-final -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 then ninja -C build-linux (~20s on drvfs). WSL Ubuntu distro provisioned (toolchain +
SDL2/zlib/GL dev); repo at <repo>
(fixed the malformed /etc/wsl.conf [automount] + wsl --shutdown).
data/ has the ROM + sidecars already.
Runtime fixes landed this session:
2afc0be7— image-base 4GiB sentinel; PE-only load-base assert guarded; MAP_32BIT game-thread stacks (kills the(u32)&stackvartruncation class — without it, crash intexLoad/memcpyat stage load).cee26d46—getenv()/realloc()K&R decls (strict host GCC truncatedgetenv's pointer ->configGetFrameDumpsegfault on theGE_PCDUMPpath).
M-40 rendering progress (commits bd7a6cde, 4d406462):
- D146 opcode-0x00 → FIXED.
-Wl,-Ttext-segment=0x20000000(CMakeLists, Linux only). Root cause: the no-PIE default base 0x400000 put.data/.bssat ~0x00500000-0x00600000, whichseg_addr()(fast3d) misreads as a K0-physical DRAM offset (w1 < 0x800000) and remaps to 0x80xxxxxx (zeroed mirror). 0x20000000 sits clear of every N64 range and stays < 2 GiB (mcmodel=small OK; 0x140000000 needed mcmodel=large -> reloc failures). ELF analogue of the Windows 0x140000000 base. start_draw_to_framebuffer(0)-> GL fb 0. Was bindingframebuffers[0].fbo, a generated attachment-less FBO -> incomplete -> strict GL (Mesa) drops all draws. Windows verified unaffected.- GE_PCDUMP capture now pre-swap (
gfx_pre_swap_hook) + GL_BACK + glFinish + PACK_ALIGNMENT=1. Post-swap readback was undefined on Mesa/WSLg.
LINUX RENDERS. (user confirmed, 2026-09-03) The user watched the live
WSLg window during a post-fix -level_09 run: BUNKER1 renders normally
(textured interior; white border = WSLg decoration). The D146 image-base
fix + the framebuffer-0 fix were the bring-up fixes.
The "black screen" I chased was only the GE_PCDUMP headless capture —
glReadPixels(GL_BACK) reads black on llvmpipe/WSLg even though the window
presents correctly. NOT a render bug. (Confirmed: a red glClear +
immediate glReadPixels in clear_framebuffer returns red, so readback of
fb 0 works mid-render; the pre-swap capture path still comes back black —
llvmpipe back-buffer semantics.) Subagent (claude, aae607dac4a1a7ce0) is
fixing gfx_opengl_dump_bound_fbo so headless capture works on Mesa
(GL_FRONT-after-swap, or blit fb0 -> a 1-attachment FBO and read that).
Watchdog false-positive fixed this session (libultra.c portHeartbeatCheck): window 3 s -> 8 s, and stop after 3 reports (was
spamming ge007.crash.log during gdb pauses / SIGTERM teardown).
Next session (M-41)
- Land the subagent's headless-capture fix, then re-verify with a Linux
-level_09framediff (a golden captured on Windows vs Linux). - Un-draft PR #8 — Linux compiles, links, and RENDERS. The user's "hold until Linux renders" condition is met once the capture fix + a framediff confirm it.
- Interactive Linux feel-check (input, a few levels), then sweep the 21 levels on Linux.
- Still owed (unchanged): D186 yield-aware frame cap; push
feat/alpha-asset-prep-tool+ PR;perf/release-optdecision; D184 F10.
Done last session (M-39) — Linux CI compile job + FpsCap 1fps diagnosis (2026-09-02, full-agentic)
Full-agentic-mode, ~2.5h. Two threads: Linux build bring-up (pushed, draft PR) and a user-reported "1 fps" regression (root-caused, user-side fix applied).
D186 — "game runs at 1 fps" = Video.FpsCap config, NOT a code regression
User reported the game (window title "GoldenEye 007 - 1 fps") had regressed to a
slideshow. Root cause: FpsCap = 10 in data/ge007.ini [Video] — almost
certainly dinged down via the F10 options overlay (feat/f10-options-overlay,
D184) while testing; ESC-close calls configSave() so it persisted. Also
ScreenShakeIntensity = 10 (default 1) in the same file, same origin.
- Bisected 14 commits back (
849ee512): identical ~950 ms/frame on this box → not a code regression. User confirms it ran fast recently → config drift. - Verified (
-level_09, headless, same 35 s window):FpsCap=10→ 5 frames total;FpsCap=30→ 5 frames;FpsCap=60→ frame ~300;FpsCap=0→ frame 900+ at ~90–160 µs/frame. Any non-zero cap degrades; lower = worse. - Applied: set
FpsCap = 0in the user's (gitignored)data/ge007.ini.-Og→-O2(see perf branch) tested too — only ~10 %, not the cause. - Underlying defect (OWED, not fixed):
sync_framerate_with_timer()inport/fast3d/gfx_sdl2.cpp(called fromgfx_sdl_swap_buffers_beginwhentarget_fps != 0) doessysSleep(left)+ a busy-wait on the render path; with the cooperative/threaded kernel this starves the game threads instead of just dropping frames. GE sim is VI-locked so a low render cap must not slow the pipeline. Fix = make that wait yield to the scheduler, and clampVideo.FpsCap(and the F10 row) to e.g. ≥ 20 or treat < 20 as 0.
Linux CI compile job — branch feat/linux-ci-compile, draft PR #8 (pushed)
Adds a real cmake --build on Ubuntu (was configure-only) + carries the M-38
romdata POSIX-mmap patch (docs/dev/LINUX-PORT.md "land behind CI"). Job pinned
to ubuntu-24.04 + gcc-14. Each fix verified to leave the Windows build green
(240/240, -level_09 crash-free):
port/shim/stddef.h— 3-way split: C++ → hoststddef; C on Windows → N64 stub (MinGW<stddef.h>drags in theerrnomacro that collides with PR/os.h); C on Linux/macOS →__SIZE_TYPE__/__PTRDIFF_TYPE__/__builtin_offsetof.port/shim/stdarg.h— Linux/macOS branch:va_list+__gnuc_va_list(glibc<stdio.h>needs the alias) +va_*from builtins, no<ultra64.h>.port/include/pc_protos.h—snprintfsize argunsigned long long→size_t(glibc size_t isunsigned long; conflicting-types error).CMakeLists.txt—-no-pieon Linux (dram_syms.s absolute 0x70000000 syms, same reason Windows needs--disable-dynamicbase);-fplan9-extensions(C, non-MinGW) for theinheritsidiom;-fpermissive(C, non-MinGW) for GCC-14 hard-error promotions (propobj.cappend_text_picked_upvestigial params).- CI progress: from ~first TU → ~19 300 lines / most of the game compiles.
Last seen failing on
propobj.c(the-fpermissivecommit targets it; result pending). Likely next:port/shim/string.h/port/shim/stdlib.hhave no Linux branch (Qwen triage, MEDIUM-HIGH) — but/usr/include/{string,stdlib}.hdo exist so extending the#elif !defined(_WIN32)→hoststring.h/hoststdlib.hroute should work (unlike stddef/stdarg those aren't gcc-internal). Then link (watch for-no-pie/ dram_syms relocs), then runtime on a real box.
Other
perf/release-opt(local, 1 commit, NOT pushed):CMakeLists.txtrestrict-OgtoDebugbuilds (was forced on all non-Debug, overriding-O2). Correct regardless; ~10 % frame time. Independent of D186.- Qwen
triagedispatched (read-only) for the Linux header-compat surface — analysis in its report (string.h/stdlib.h shims flagged); thedocs/dev/LINUX-HEADER-COMPAT.mddeliverable was not written (hit turn limit).
Next session (M-39)
- Watch PR #8 CI, keep clearing Linux compile breaks (string.h/stdlib.h shims next), then the link step, then runtime bring-up on a Linux box. PR stays draft — human merges.
- D186 real fix: yield-aware frame cap in
gfx_sdl2.cpp+FpsCapclamp; land with the F10 overlay branch (that's where the footgun is). - Decide
perf/release-opt— trivially mergeable, or fold into another PR. - Still owed from M-38: push
feat/alpha-asset-prep-tool+ PR (rebase to main); D184 F10 interactive feel-check.
Done this session (M-38) — alpha asset-prep tool + Linux romdata + D185 blocker (2026-09-02)
Branch feat/alpha-asset-prep-tool (off docs/dropin-release-plan-and-status),
2 commits, not pushed, no PR. Full-agentic-mode session, ~60 min.
-
Alpha asset-prep tool — DONE + verified.
tools_pc/dist/prepare-assets/prepare-assets.py(stdlib-only: hashlib/shutil/subprocess/argparse/pathlib). Auto-detects the ROM near the bundle, SHA-1 → region (US/EU/JP, rejects byte-swapped/overdumped), runsd43_emit.py+d69_emit.pyagainst a vendored input tree, writesdata/pcmodels-<region>/+data/pccg-<region>/next to the exe.bundle-win.shassemblesprepare-assets/fresh at package time (emit scripts fromtools_pc/, +filelist.u.csv+file_resource_table.inc.c+ 512modelFileHeader.inc.cas vendored inputs — decomp metadata, not game assets). Bundled README gets a one-time "Step 2:python prepare-assets\prepare-assets.py". Verified end-to-end: from a bundle with only exe+DLLs+US ROM, produces all 4 files byte-identical tod43/d69run from repo root; bundle 14.1 MB (< 60 MB guard); no-ROM / bad-SHA error paths tested. Resolves BACKLOG "Alpha release — asset-prep tool" steps 1–3. Step 4 (clean-machine smoke with the game running) is BLOCKED by D185 ↓. -
D185 — investigated + RESOLVED same session (
4491db3f). First cut ofprepare-assets.pyran only d43 + d69, sopccg.binwas missing the 21Usetup*Zper-level stage-setup files thatd88_emit.py --regenappends to the same dir → every level segfaulted pre-frame-1. Not a d69/input regression (the CRLF→LF change tofilelist.u.csv/file_resource_table.inc.cfrom the public-release migration is real but d69 tolerates it). Fix: pipeline is now d43 → d69 → d88 --regen;bundle-win.shvendorsd88_emit.py+ its local importd88_propdefs.py. Verified byte-identical to the known-good sidecar +-level_09/-20crash-free. -
build-pc/+data/state (heads-up): this session did a full clean rebuild ofbuild-pc/fromfeat/alpha-asset-prep-tool(docs branch + romdata + tool), and replaceddata/pcmodels-ntsc-final/+data/pccg-ntsc-final/with freshly-generated copies (verified byte-identical to the prior working ones). To get back to another branch's build:git checkout <branch> && rm -rf build-pc && ./build-pc.sh ntsc-final. -
Linux romdata map — landed (compile+runtime verification owed).
port/src/romdata.c: the0x10000000cart-base map was Windows-only (VirtualAlloc); the POSIX#elsewas a "not implemented" stub. Added an anonymousmmap(MAP_FIXED_NOREPLACE)atCART_BASEwith identical heap-copy fallback on failure; factored the shared post-map work intoromdataFinishCartMap(). Windows: builds 242/242,-level_09crash-free, no behaviour change. Linux compile +mmap-lands-at-0x10000000 + runtime are owed on a Linux box — full gap analysis indocs/dev/LINUX-PORT.md(Qwen triage was wrong on 2/3 headline items; the CMakeelse()branches andsystem.cPOSIX paths already exist — romdata was the one real code gap).
Next session (M-38)
- Push
feat/alpha-asset-prep-tool+ PR — the asset-prep tool is done and verified end-to-end (d43+d69+d88 from an assembled bundle = byte-identical sidecars, levels boot crash-free). Rebase ontomainfirst (it currently sits on the docs branch). Folddocs/dev/LINUX-PORT.md. BACKLOG "Alpha release — asset-prep tool" step 4 (clean-machine smoke) is effectively done headless; a real human run from an unzipped bundle is the last confirmation. docs/building.mdgap: it should tell source builders to rund88_emit.py --regentoo (it already saysd43/d69) — same omission that caused D185.- Linux: turn the CI Ubuntu
validatejob into a real compile job (same PR as the romdata mmap patch) → then runtime bring-up on a Linux machine (docs/dev/LINUX-PORT.mdhas the checklist). - D184 F10 overlay interactive feel-check still owed (unchanged from M-37).
Done this session (M-37) — F10 options overlay (approach C) (2026-09-02)
Branch feat/f10-options-overlay, port-layer only, not pushed, no PR.
Finding D184. Implements docs/dev/OPTIONS-MENU-PLAN.md §2 approach (C).
- New:
port/src/optionsoverlay.c,port/include/optionsoverlay.h— an immediate-mode overlay drawn as its own fast3d 2D DL appended after the game DL. Closed ⇒optionsOverlayEmit()returns NULL, nothing appended, golden dumps byte-identical. - Hooks:
port/fast3d/gfx_pc.cpp gfx_run()(append aftergfx_run_dl, beforegfx_flush);port/src/video.c videoPumpEvents(F10 toggle next to F12; ESC closes; wheel → scroll;videoRequestLiveConfig()+liveCfgDirtyapply of VSync/FpsCap/TextureFilter atvideoStartFrame);port/src/input.c inputComputePad(controller 0 swallowed + nav routed while open — mirrors the D180 WI-1 pattern);port/src/config.c+port/include/config.h(configForEachOption+configSetOptionMetaside table,CONFIG_OPT_*enum). - v1 rows: VSync, FpsCap(0–360), MSAA(1/2/4/8, restart), TextureFilter,
MouseAimSpeed, MouseTurnSpeed, MouseInvertY, MouseCaptureMode,
ScreenShakeIntensity(0–3). Left/right adjust; live where applicable;
configSave()on close. - Diagnostic:
GE_OPTIONSOVERLAY=1auto-opens at boot (env-gated, left in). - Verified: build links 241/241. Runtime checks NOT done — this
headless env kills the GUI process after ~8 s and
gfx_opengl_dump_bound_fbowrites 0-byte PPMs, so-level_09framediff, the 60 s soak, and the overlay-frame eyeball are owed on an interactive machine. - Owed feel-check: open F10 in-level and in the front end; confirm layout sanity, live VSync/filter apply, and that closed = no visual change.
Also M-37 (on main, committed):
- PR #5 (D177 ladders) confirmed merged (
955349db); branch + both agent worktrees removed; D177 fully closed (climb test no longer owed). - D176(a) black sky — ROOT-CAUSED (
4acadada, findings.md "D176(a) — M-37 UPDATE").gfx_pc.cpp:2901deliberately no-opsG_RDPHALF_*; GE's cloud/water sky emits geometry only as that stream. No PD shortcut. Fix = decode the RDPHALF sky-tri stream in fast3d (~1 session). Scratch:D176a-sky-rootcause.md. - D176(b) — offline ROM decode blocked (§7 toolchain absent). Next: grep the
converted
bg_sevx_all_pGDL for opcode 0xC0, then runtime GE_DTEX image-ID correlation. Scratch:D176b-rom-groundtruth.md. - Commit trailers stripped from the F10 branch per the no-session-trailers rule.
Next session
- Finish D184: interactive machine — F10 framediff (
-level_09), 60s soak, overlay-frame eyeball + nav feel. Tune row coords / arrow-repeat if needed. Then pushfeat/f10-options-overlay+ PR. (Contains one stray commit3c419e8dD177-doc-tidy — harmless, merges fine.) - OR D176(a) fix: first
-level_22/-level_29headless to confirm also black (proves emit path), then write the RDPHALF sky-tri decoder inport/fast3d/gfx_pc.cppper the scratch note. mainhas 3 uncommitted doc edits (Qwen 3→3.8, mermaid setup) — the user's own concurrent work; leave for them to commit.
Done this session (M-35) — native-PC input + options-menu QoL run (2026-09-02)
Branch qol/native-pc-input-menu (from def0e0ac), draft PR open,
nothing merged to main. Full review: docs/dev/M-35-QOL-REVIEW.md;
playtest handoff: docs/dev/M-35-PLAYTEST-CHECKLIST.md; WI-3 design:
docs/dev/OPTIONS-MENU-PLAN.md. Findings D180 (input), D181
(screen-shake hook). Commits 91b7c5aa → 849ee512 (+ docs).
- WI-1 (D180) — DONE, feel-check owed.
Input.MouseCaptureMode(0 = legacy always-grab, default; 1 = Quake-style click-to-lock).port/src/input.c+video.c, port-only, no#ifdef PORT. Mode 1: free OS cursor until you click the window; ESC / focus-loss / entering a front-end menu frees it; re-entering a stage while armed re-locks.reconcileGrab()per poll offcurrent_menu. Mouse buttons withheld from the game while free in-stage. Controller paths untouched. B3:Input.MouseAimSpeeddefault 25 → 16. - WI-2 (D180) — DONE for capture mode, feel-check owed. When capture
mode is on and the cursor is free in a menu, the D165/D169 pointer
P-controller takes its target from the absolute OS cursor position
mapped onto the live virtual front-end rect → true 1:1, no drift. Route
(a), port-only. Legacy relative path unchanged; 1:1 menu tracking
requires
MouseCaptureMode = 1. - WI-3 — foundation only.
configRegisterFloat/configRegisterUInt- clamps landed (
port/src/config.c);configSaveunified. First route-(b) hook:Game.ScreenShakeIntensity(D181,src/fr.c viShake,#ifdef PORT, default 1.0 = exact no-op). The options-menu surface is a design doc only (OPTIONS-MENU-PLAN.md) — recommended a port-layer fast3d 2D overlay (F10), not asrc/menu edit; resume checklist in that doc.
- clamps landed (
- Verified: build 241/241;
-level_09framediff 3/3 within threshold (nonclear Δ ≤ 0.01pp, phash noise = documented D117);GE_STARTMENU=7boot crash-free; golden dumps unaffected at defaults. - Resume point for a tuning session: run
M-35-PLAYTEST-CHECKLIST.mditem 0 first (default-config regression), then the click-to-lock and aim-speed feel items. IfMouseAimSpeed = 16still overshoots, drop the default further inport/src/input.cand note it on the PR.
Where things stand
- BUNKER1 (
-level_09) is playable-ish. Loads, renders recognisably (textured rooms, storage racks, floor; skeletal guards render as humanoids), and survives a guard firefight — 45 s+ crash-free (D103–D120). Silo (-level_20) also loads + renders clean. Committed throughf2beae4b(M-12: D121 WS1 boot, D122 propDef fix). - Input layer (Phase 3) landed and playtested (D118, M-9/M-10).
port/src/input.cis real: keyboard+mouse + SDL_GameController → N64 pad. Core aim/move works. M-24 mouse-look rework (port/src/input.c, port-only): mode-aware map — aim mode (RMB) drives the analog stick past ±60 and emits no C-buttons; hipfire keeps digital C-up/C-down pitch. D118b (mouse-Y inverted) and D118c (aim + mouse-down → crouch) FIXED; D118a residual — hipfire pitch still digital vs analog yaw (minor).config.cINI load/save now implemented →ge007.iniis written on first run and re-read;[Input]MouseEnabled/MouseAimSpeed(50) /MouseTurnSpeed(100) /MouseInvertY(0) are live-tunable. Rebinding / gamepad hotplug still TODO. Weapon switch on kbd = A button (Space/Z/E) — no dedicated key. - Recent fixes (M-10,
d1e93b76): D119 guard-attack crash (weapons_held[]->chrtype-pun) fixed; D120 blood-stain hang guarded (not fixed —d43_emit.pyopcode-0x18 converter gap). - Cosmetic backlog (parked,
docs/GRAPHICS-BACKLOG.md): D75 front-end 3D model transforms (Nintendo logo misplaced, gun-barrel Bond absent, cast models absent), D76 disclaimer screen partial, D77 no audio, D74 dead wrap-block. D114/D116 "HUD/text X-mirror" — CLOSED, NOT A BUG (M-33/D168): everyGE_PCDUMPwas vertically flipped; the game renders correctly on hardware. PPM writer fixed.
Done this session (M-33 part 2) — build unblocked, D168/D169 verified (2026-09-01)
Environment sorted. The build works from the migrated tree: PATH=/c/msys64/mingw64/bin
- ROM copied to
data/ge007.ntsc-final.z64+ the three sidecar regens (d43_emit.py/d69_emit.py/d88_emit.py --regen, all pass clean) →./build-pc.sh ntsc-finallinks 242/242,-level_09runs crash-free past frame 1200. Noassets/*.binextraction needed — the committedassets/is self-sufficient; the ROM is runtime-only. The old repo at<repos>/007has the ROM + a prior build.
- D168 — VERIFIED. Rebuilt with the flip fix, captured
-level_09frame 320: the exit arrow points up, ammo "7|93" reads normally bottom-right — matchesdocs/img/bunker1-2.png. The legal screen, RAREWARE logo and level-name captions all render upright with normal text. D114/D116 conclusively closed as capture artifacts.tools_pc/golden/*.pngregenerated from a fresh correct run (framediff.py --update) — stopgap flip replaced. - D75 — SHRUNK. Upright bare-front-end capture (
GE_PCDUMP="700-1200:25"): gun-barrel Bond renders and animates fine (frame ~1075 walk, ~1150 fire, upright tuxedo silhouette in the iris). The "Bond absent" reports were the flipped capture. Nintendo logo IS genuinely broken — frame ~775 shows two overlapping white ellipsoids shifted left, no wordmark (reallogoinsttransform/geometry bug, not a flip). Cast roll not re-captured. Annotated infindings.md§F "D75 Bug 2 — M-33 UPDATE" +GRAPHICS-BACKLOG.md. - D169 — FIXED (
940f9426,port/src/input.c, port-only). Menu pointer clamp bounds now come fromgetPlayer_c_screenwidth/height/left/top()(the live 440×330 front-end field) instead of hard-coded 320×240; estimator seeds from the realcursor_h_pos/v_pos. Builds + links clean;GE_STARTMENU=7and-level_09crash-free; framediff 3/3. Interactive feel-check owed — headless can't move the mouse, so "every grid tile reachable" is inferred from the corrected clamp math. - Task 3 — attract screenshots captured.
docs/img/attract-{bunker1,silo,dam}.pngcommitted (ea98a4b3), upright, pre-spawn intro-camera views. Not wired into the README (developer to place; keeparchives-1/bunker1-1as the top pair).-level_24(Archives perplaytest.sh) booted to an outdoor rocky area, not the interior — didn't ship it; worth a look.
Commits this session: 7ce4fead 06d3eb22 7292fc0e (part 1) → a0c7f2f5
(developer: operating manual + doc cross-refs) → 940f9426 ad134615 ea98a4b3.
Next
- Interactive feel-checks owed: D169 (mouse reaches every level tile; file-select/main-menu unregressed) and the M-24/M-31 mouse-look knobs.
- D75 residual: Nintendo logo
logoinstmodel renders as white blobs — real transform bug; recheck cast roll withGE_PCDUMP="1400-2200:25". - RC1 (wallet-Bond photo "180°"): needs a mode-select capture — interactive nav, not done. Re-judge against upright.
- Wire the attract screenshots into the README once placement is decided.
-level_24boots to the wrong area — investigate.
Done this session (M-33 part 1) — public-repo migration smoke-test + honesty pass (2026-09-01)
First session in the migrated public tree (cleaned history, docs/ restructured
into docs/ + docs/dev/). Migration findings from part 1:
- Doc paths were stale —
CLAUDE.md/AGENTS.mdreferenced pre-restructure filenames (PORT-LEARNINGS.md,PCPortResearch.md,AGENT-WORKFLOW.md). Fixed by the developer ina0c7f2f5. - Task 1 — doc honesty pass (
7ce4fead).README.md: "Not playable yet" callout; Status platform bullet rewritten (Windows-only testing; Linux build unverified at runtime); Linux build steps flagged untested;building.md+ci.ymlcomment clarified. - Task 2 —
GE_PCDUMPvertical flip = D168 (06d3eb22). Writer fixed; D114/D116/D75 annotated acrossfindings.md,GRAPHICS-BACKLOG.md,TEXTURE-GLITCH-ANALYSIS.md,porting-notes.md §D2,GE-ENV-PROBES.md. - Task 4 — D169 root-cause writeup (
7292fc0e) — 320×240 vs 440×330 clamp mismatch in the menu pointer P-controller.
Done this session (M-32b) — tactical follow-up (2026-08-31)
3 commits on master (49ce620a, 7071b110, + this reconciliation).
- Debug-scaffolding strip (
49ce620a). Removed inert env-gatedGE_D54/GE_D63/GE_D90probe blocks from compiledsrc/(memp.c,bg.cincl. the bare non-PORTd63bgprimarycountstatic + entry log,blood_animation.c,dyn.c,rsp.c,bondview_r.c,csplayer.c,load.c,seqplayer.c; 161 lines).GE_D54/GE_D90now fully gone;GE_D63partially (remaining blocks all#ifdef PORT+ getenv, inert — strip candidates for later). Build green;-level_09+-level_20render crash-free (19 frames each).docs/GE-ENV-PROBES.mdupdated. - D75 Bug 2 runtime probe (
7071b110).GE_D75=1intitle.csub_GAME_7F007F30. Booted bare front end through the gun-barrel: Bond + gun models absent; both Model instances valid (nMtx 21/1);render_posvalid + fresh each frame (== that frame'sdynAllocatemtxlist base, clean double-buffer, o2p unchanged) → the "stale arena" hypothesis (D115 #5) is RULED OUT — do not land a persistentrender_posbuffer. Zero fast3d DL warnings → not D144/D146 either. Failure is downstream indrawjointlist/dotubevtx/node-DL resolution or an off-screenbasemtx. Next: a drawjointlist-level probe. Full write-up: §F "D75 Bug 2 — RUNTIME PROBE". - RC3 Depot eyeball — INCONCLUSIVE, default stays OFF. Captured Depot
(
-level_30) withGE_WRAPFIX=1vs0. Depot is too dark in the boot-camera area to judge the non-PoT (65×65 / 96×48) ceiling/panel surfaces the fix targets; the visible corrugated-container walls look unchanged and un-regressed with the fix on. Not enough signal to flipcfgWrapFix=1. Needs a live human eyeball on Depot withVideo.WrapFix=1(walk to a lit area / the ceiling), or aGE_DTEXcapture on the specific surface.port/src/video.ccfgWrapFixunchanged. - Sidecars regenerated (
d43_emit.py/d69_emit.py/d88_emit.py --regen, ntsc-final) — ready for a campaign playtest.
Next (M-32b)
- RC3 — human eyeball Depot with
Video.WrapFix=1; flipcfgWrapFix=1inport/src/video.cif a lit non-PoT surface is visibly better and-09/-20/-34don't regress. - D75 Bug 2 — drawjointlist/
dotube-level probe (resolved vtx/nodeDl ptrs, whether any tris emit, composedbasemtx*render_posfor joint 0). - D143 blank briefing/objective text — untouched this session.
- D154 / D152+ / D160 — still playtest-gated.
Done this session (M-31/M-32) — 6-agent parallel burst #2 (2026-08-31)
6 worktree subagents, files partitioned; 5 merged to master
(0c918ab4..1753ac7a cherry-picked, then a reconciliation commit).
Combined tree builds clean (240/240); -level_09/-level_20 render
crash-free (framediff phash = documented D117 intro-pan noise, nonclear
coverage stable to 0.04pp); 6/6 texture-heavy levels (Depot/Facility/
Runway/Archives/Streets/Caverns) boot+render+no-crash.
| Item | Result | Verified |
|---|---|---|
D154 (bg.c) |
Existing wall-shoot GBI-parser port was mostly right, but vtxoff = gdl->dma.par & 0xf was always 0 — the PC Gdma_le shim maps .par to word0 bits 0-23 (packed length), not the N64 params byte. Fixed → ((u32)gdl->words.w0 >> 16) & 0xf. Also ported a sibling raw parser: bgTestBulletHitBackground G_SETTILE back-scan (~3841). GE_D154=1 diag added. |
build 242/242; playtest-gated (needs firefight into a wall on an idle machine) |
D152+ (snd.c, libultra.c) |
Audit: every compiled-audio osSetIntMask(OS_IM_NONE) is balanced — the §F "unbalanced early-return" guess was wrong. Real fixes: sndSetSfxSlotVolume now holds the mask across its ALSoundState walk (matches its twin sndDeactivateAllSfxByFlag) + sndApplyVolumeAllSfxSlot batches its loop under one recursive hold (kills the mission-failed fade lock-storm); portThreadWrapper → imThreadExitRelease() releases an orphaned lock on thread exit (kills the transient-thread-died leak + pthread-id-reuse re-wedge). Steal-lock kept as backstop. |
build 240/240; -level_09/-20 crash-free; fade-out repro playtest-gated |
RC3 / D167 (gfx_pc.cpp) |
fast3d never stored the N64 tile mask — wrapped every repeating texture at GL image size, not 1<<mask (wrong period on Depot's 65×65 / 96×48 surfaces). Fixed behind Video.WrapFix (default OFF = byte-identical to golden). GE_WRAPFIX=0/1 override. |
per-level captures clean, no regression; needs a human eyeball on Depot with Video.WrapFix=1 before default-on |
| D75 (docs) | Option (a) — D73 gu/float-endian scope gap — ruled out (gu/* + matrixmath.c fully endian-clean; Rareware logo exercises the whole path and renders fine). Two independent bugs: Bug 1 (logo misplaced, photo 180°) = the parked D114/D116 fast3d mirror; Bug 2 (gun-barrel/cast models absent) = independent, likely model->render_pos → transient dynAllocate arena (D115 item #5). Needs a runtime probe. |
write-up only |
| D160 / D148 (docs) | "propDef command-index walk desync" hypothesis DISPROVEN — exhaustive static trace: every record type Dam emits has converter PC bytes == sizepropdef()×4, stream tiles byte-exact, walk in lockstep, all 21 levels clean. Dam rappel cutscene is runtime AI-script / CAMERAMODE_POSEND cinematic render (D75 family). GE_D160=1 diag already ships. |
needs live Dam-to-exit playthrough |
| Docs | TEXTURE-GLITCH-ANALYSIS.md §0 status table reconciled (RC2 FIXED / RC4 RETRACTED / D159+D161 FIXED / RC3 done-behind-knob); new docs/GE-ENV-PROBES.md (full env-var table); GE_D63 strip-safety checklist (all inert; only bg.c:2876/2880 is a bare non-PORT static + entry log worth deleting for N64 hygiene). |
— |
Worktree-harness note: 3 of the 6 agent worktrees were created from a
stale commit (0a4b3bae, ~30 behind); a mid-flight git reset --hard master per agent recovered them. Watch for this on future bursts — check
each agent's HEAD in its first report.
Next
- D75 Bug 2 — runtime
GE_PCDUMP+ capped probe on the front-end animated-modelrender_pos/dynAllocatepath (the one in-scope lead). - RC3 — eyeball Depot with
Video.WrapFix=1, flipcfgWrapFix=1inport/src/video.cif clean. - D154 / D152+ / D160 — all playtest-gated; verify during the campaign playthrough (wall-shoot / mission-fail / Dam exit).
- Strip the
bg.c:2876/2880bare D63 entry log.
Done this session (M-12) — 2 commits
- WS1 / D121 (
02c12068) — bare./build-pc/ge007.x86_64.exe -level_XXauto-injects that stage'smemallocstringtable[]-m*pool row (#ifdef PORTinboss.c bossInitMainthreadData). No manual-m*args any more. Gotchas recorded: settingg_DebugAndUpdateStageFlag=1is the WRONG fix (routes boot through the title-stage intro), andtokenSetStringreplaces the whole token buffer so the injected string must carry-level_XXforward. See §H D121. - D88.4 was already resolved (committed
ff563812/4ccc0d3cweeks ago). The "OPEN — cross-level blocker /setupDoorcrash" note in the old §F index / plan was stale.d88_emit.py --regen= 21/21 pass. - D122 (
f2beae4b) — the real cross-level blocker was prop/item model loading.tools_pc/d88_propdefs.pyhad a per-type handler table + a generic fallback; 6inherits ObjectRecordpropDef types (47 TINTED_GLASS, 39 VEHICHLE, 40 AIRCRAFT, 45 TANK, 13 AUTOGUN, 20 AMMO/MultiAmmoCrate) had no handler → fell to the generic word-granularbswap32, which swapped the[s16 obj][s16 pad]word as one u32 → model idobjin the wrong half → OOBPitemZ_entries[]deref → crash inmodelLoad(loadobjectmodel.c:393) /modelInitRwData(model.c:6249). Fixed in the converter (OBJ_TAIL_DESChandler) + matchingsizepropdef()#ifdef PORTstride. BUNKER1/Silo were never affected (they don't emit those types). Full write-up: §F/§H D122. Confidence: crash fix high; types 39/40/45 tail layout medium (structs have "locs unconfirmed" notes — nudge if a level actually drives a tank/vehicle).
Uncommitted docs (tangled with the in-progress big docs restructure —
HANDOFF.md, AGENTS.md, CLAUDE.md): the D121 + D122 entries in
docs/PCPortResearch.md §F/§H and this file's edits. Commit or fold in.
Done this session (M-17) — 4 commits, 13→18/21 PASS
- D126 (
e851e2ab) — objective sub-recordscriteria_picture(30),criteria_roomentered(32),criteria_deposit(33),setup_objective_text(35) each end in aT *nextlist pointer thatset_parent_cur_obj_*/setup_briefing_text_entry_parentwrite while walking the setup stream. Widens 4→8B and 8-aligns at offset 16 on PC → struct is 24B/6w (N64 16/20); the old N64-sized emit meant the runtime 8-byte->nextstore clobbered the next propdef record's header → walk desync → command indices drifted ~100 →setupDoorlinkedDoorresolution landed on the wrong record. Fixedd88_propdefs.py(PROPDEF_PC_BYTES[30/32/33/35]=24+ typed handler) +loadobjectmodel.csizepropdefPORT returns 6. **Cleared C3r Bunker2- C4 Depot + C6 Surface2** in one change.
- D127 (
1129f63d) —sndPlaySfxguards a bogusALSound*(the converted libaudio bank has fewer/rearrangedsoundArrayslots than N64; audio is Phase-3 parked). Cleared C7 Surface1. - D128 (
749feb9f) —sub_GAME_7F0B37ECspecial-portal marker used the hardcoded N64 8-bytebg_portal_data_entrystride /controlbytes1@6; on PC the struct is 16B withcontrolbytes1@10, so|= 2scribbled into the middle of a portal'soffset_portalpointer →bg.c:5723crash on any level with aspecialportalarrayentry.#ifdef PORTuses the struct accessor. Cleared C5 Control. - D129 (
<this commit>) —langGetbounds-checks the slot id and resolved bank pointer. Bare-level_XXboots that reach the cast/credits text path (Cuba,bondviewRenderCredits, D76 area) reference banks the menu flow never loaded. Reduces the Cuba bare-boot end-credits crash; Cuba itself loads + renders 300+ frames fine and the credits work through the real front-end flow.
Done this session (M-18) — 1 fix, 18→20/21 PASS
- D130 (
port/src/romdata.c, uncommitted) — Facility-level_34+ Runway-level_35C2 crash (import_texture_i8AV on wild ptr0x72181ee8). Root cause was NOT the model-GDL relocation the D124-Facility addendum /BRIEF-C2gdl-model-reloc.mdsuspected — disproved:texLoadFromGdlnever copies aG_SETTIMGon these levels,texWriteLoadToTmem*is never called, andgdlinsub_GAME_7F0762E0is still a segmented0x05xxxxxxvalue so its& 0x00ffffffmasks are correct. Real cause:romdataFixupFontcorrupts BankGothic/ZurichBold glyph indices 0/1/2 — the in-place N64 24B → PC 32Bfontcharrelayout aliasesdst/srcfor low glyph indices (stride grew 8B < the 24B field read span), so a field write clobbers a later field's source mid-loop (glyph 1width=bswap(index), glyph 2pixeldata≈0x020002e8→+= font_base→ wild). Facility's title "Chemical Warfare Facility #2" renders#→gDPLoadTextureBlock(gdl, curchar->pixeldata=wild, …)→ fast3d AV. Fix: stage all 6 N64 fields in a localf[6]before writing. §F/§H D130; PORT-LEARNINGS §A. Verified: -level_34 0/~14, -level_35 0/4; -level_09/20/24 unregressed.
Done this session (M-19) — D131 (Jungle), D63 scaffolding removal
- D131 (
port/fast3d/gfx_pc.cpp) — Jungle-level_37C2m crash.explosionRenderPropSmokepasses a compiled.bssmatrix symbol (&dword_CODE_bss_8007A100) throughosVirtualToPhysical()— au32-returning shim — intogSPMatrix, truncating the0x1_00000000module high word → w1 =0x401c68e0→ wild deref ingfx_sp_matrixwhen the first explosion draws (~frame 300). NOTgimgfixup/ the model-GDL path (that premise was already dead per D130). Fix:seg_addrrestores the module high word for a fallthrough w1 in[0x40000000, 0x70000000). Covers ~30 latentosVirtualToPhysical(<compiled matrix/vtx symbol>)sites (explosion/glass/blood/bondview2). Verified: Jungle renders to frame 1500–2400+ crash-free;-level_20/-level_24unregressed. §F/§H D131, PORT-LEARNINGS §A. - D63 scaffolding removed — dead TEMP D63 probe calls stripped from
port/src/libultra.c(watchdog thread + activity ring + slot watchers) and the#ifdef PORTprobe blocks inblood_animation.c,dyn.c,front.c,rsp.c,title.c. All inert (env-gated / no side effects); build links clean.port/fast3d/gfx_pc.cppD63 trail code left for a later pass. Bug class D24-implications +osYieldThreadwatch item added to PORT-LEARNINGS §E.
Done this session (M-23) — WS6 Facility playthrough: D135/D137/D138/D139 FIXED, D136/D140/D118c open
First real human WS6 playtest. User speed-ran Dam OK, then hit a chain of
crashes on Facility (-level_34) — each one a subsystem the 21-level sweep
never exercises (no weapon fire, no stage unload, no pause menu). Fixed four,
two new open, Facility now near-complete. Committed ec51d9c5 (the
older D121/D122/D126/D128/D130/D131 write-ups are still uncommitted, tangled
in the docs-restructure — fold those separately).
- D135 (FIXED)
src/game/propobj.c— firefight crash.bgTestHitOnObj(bullet-ray vs object triangle geometry, viapropobjFindHiton every shot that resolves on an object model) is an unported N64 GBI parser: raw 8-byte-Gfxbyte/word indices desync on the PC 16-byteGfxstream → walks off the DL → AV0x709ae02a. Ported#ifdef PORTtogdl->words.w0/ .w1shifts (mirrors PDpd_port/src/game/bg.c:3635). Second fault after the parser fix:objHitpropobj.c:9717% impact_sounds->thing2_lenint div-by-0 — the ported texnum-recovery (*(s16*)(phys(w1)-8)) returned a bogustexturenum→ OOBg_HitTypeSounds[]. Fixed: PORT texnum branch always-1(isnd_default, safe; generic impact sound/decal, park w/ D77). - D137 (FIXED)
src/game/gunfire.c— right-mouse (raise crosshair) crash.gunDrawSights32 sp54holds aGfx*the whole fn (texSelect/display_image_at_positiontakeGfx**); 4-byte slot →*(Gfx**)&sp54splices adjacent stack → wild DL write intexSetRenderMode.#ifdef PORT:sp54isGfx *. §A. - D138 (FIXED)
src/snd.c— the "1 room from the end" freeze (not a crash — kernel-heartbeat watchdog).sndCreatePostEvent→alEvtqPostEvent(libultra/audio/event.c:110) walksevtq->allocList; parked audio thread never drains it,chrobjSndCreatePostEventposts per objTick per ambient-sound object, Facility is dense with machinery → the list walk stalls the main thread.#ifdef PORT:sndCreatePostEventis a no-op until audio lands (D77). No gameplay effect. - D139 (FIXED, UNVERIFIED)
src/game/cleanup_objects.c— stage-unload crash (lvlUnloadStageTextData→cleanupObjects→objFree,obj->prop= packed-float garbage). Root cause:cleanupObjectswalks propDefs with(u8)obj[0]for the type, which works only because the header word[u16 extrascale][u8 state][u8 type]is big-endian (type = low byte). On LE the low byte isextrascale→ the walk never matchesPROPDEF_END, runs off the blob, dispatches the switch on garbage →objFreePermanentlyon non-objects → crash.#ifdef PORT: use((PropDefHeaderRecord*)obj)-> type(offset 3) like every other consumer (sizepropdef, proplvreset). Not confirmed — when the user hit pause to trigger a fast teardown test, the pause menu itself crashed first (D140).-level_09unregressed (91.65%). §F D139, PORT-LEARNINGS §C (BE header byte on LE). - D140 (OPEN) — pause menu crashes.
maybe_mp_interface→bondviewRenderWatch(bondview2.c:8604) →bondviewTransformManyPos- ToViewMatrix(g_CurrentPlayer->field_23C = NULL, objheader->numMatrices=9)→matrix_4x4_copy(src=0x0)AV. The watch model's per-node view-matrix cache (struct player.field_23C) is never allocated (orfield_23Cis at the wrong PC offset —struct playerlayout,docs/AUDIT-M6-player- offsets.md). Blocks the WS6 pause-menu / objective-status checks and the watch gadgets. D75 3D-model-transform family. Files:src/game/bondview2.c,src/bondtypes.h(struct player). - D118c (OPEN, low pri) — in manual-aim mode, mouse-down → crouch.
Mouse Y emits C-down;
bondview2.c:5351maps C-down ininsightaimmodetocrouchDown/zoom as well as aim pitch. No input-layer-only fix; needs the deferred analog-aim#ifdef PORThook (§F D118). Documented, parked. - New harness
tools_pc/repro_gdb.sh <XX>— nohup-launch a level, attach gdb, dump fullbt full+ locals on the first SIGSEGV/SIGFPE, rolling frame dump toppm/. Needed becausege007.crash.log's own backtrace is#01 = null(native unwinder fails). Alsobuild-pc/d136_cmds.txt— a gdb cmd file that traces everyobjFreecall (type/obj/prop/model) then catches the fault; the "last line before FAULT" localises D136-class walk-off bugs. Attach the winpid viaps -W | grep ge007.x86_64 | awk '{print $4}'—repro_gdb.sh's ownps -p $!derivation is wrong. - Follow-up (not done): the BG room-geometry hit-test in
bg.c(~3373-3646, walks the D85-widenedptr_expanded_mapping_info) is the identical unported GBI parser to D135 — latent, fires on shooting walls/floor. Same mechanical port. §F D135 + PORT-LEARNINGS §B. - Controls (for the new session's own playtesting): kbd — WASD/arrows
move,
A/Dsidestep, mouse X turn (analog), mouse Y look (digital C-button), LMB/LCtrl fire, RMB/LShift aim,Space/Z/E= A (action AND weapon-switch — GE overloads it),X/R/F= B (reload),Q= L,Enter/Tab= Start,Escquit. No dedicated weapon key.
Done this session (M-24) — mouse-look rework + real INI parser + playtest QoL
Pre-playtest QoL, port-layer only, no src/ / game-logic change.
Full review sheet: docs/M-24-QOL-REVIEW.md (rationale, per-change
risk, verification, what to check by playing). Committed f3ec5170,
65ed0315, 33506aee.
port/src/input.cmouse-look rework. ReadbondviewProcessInput/MoveData: GE aim is mode-dependent — hipfire yaw = analog stick-X, pitch = digital C-up/C-down (stick-Y = move, no pitch); aim mode (R held) yaw+pitch = analog stick past ±60, and C-up/C-down there = crouch/lean/ zoom, not aim. New map keyed on our own RMB/LShift (hold-to-aim proxy): aim mode pushesstick_x/yinto the 61..80 band and emits no C-buttons; hipfire keeps digital C-pitch. GE's native pitch is inverted (C-up→look down) — hidden so mouse-down looks down;MouseInvertYflips.- D118b FIXED (mouse-Y inversion), D118c FIXED (aim+down→crouch —
no
src/hook needed, aim look no longer emits C-down). - D118a residual: hipfire pitch digital vs analog yaw. Minor (precise
vertical aim is an aim-mode activity). Full fix = the deferred analog
#ifdef PORTbondview.chook.
- D118b FIXED (mouse-Y inversion), D118c FIXED (aim+down→crouch —
no
port/src/config.c— INI load/save implemented (was a stub that only logged TODO). Parses$S/ge007.ini([Section]blocks,Key = value,#/;comments), clamps ints to registered bounds, writes defaults on first run, rewrites on clean exit. Unblocks live tuning of the mouse knobs + all future video options. Note: constructor-registered options present atconfigLoad()time (main.c:52) are captured; anything registered later would miss the first save.- New knob
Input.MouseTurnSpeed(hipfire yaw %, default 100), split fromMouseAimSpeed(aim-mode %, default 50) since they feed different game curves. - Verified:
-level_09boots crash-free to 900+ frames @ 91.65% coverage (unregressed);-level_20crash-free;ge007.iniwritten then re-read with no unknown-key warnings. Build green (ntsc-final). port/src/video.cQoL: F12 takes a screenshot (ppm/shot_NNN.ppm, dumped on the render thread via the existinggfx_opengl_dump_bound_fbo); window focus loss frees the mouse (inputSetMouseGrab(0)onSDL_WINDOWEVENT_FOCUS_LOST, re-grab on gain) so alt-tab works.inputSetMouseGrab()also zeroes the aim delta and suspends mouse reads while released.- Alt-Enter fullscreen toggle already existed (
gfx_sdl2.cpp:299). - Gamepad hotplug —
SDL_CONTROLLERDEVICEADDED/REMOVED→inputRescanPads()(close all + re-open). Caveat: the game latchesinputConnectedMask()atosContInit(boot), so a pad added later merges into controller 0 for play but won't appear as a separate channel — plug it before launch for multi-pad. - Mouse-wheel = weapon cycle — a notch queues a 2-poll A-button press;
GE's default scheme (
invButtons = A_BUTTON,bondview2.c:5162/5326) cycles the weapon forward on a bare A edge. Both wheel directions cycle forward (no clean backward input without the A+fire combo). - Window title shows live FPS (
wmAPI->set_window_title, ~1 Hz). - Still TODO (not started): key rebinding, on-screen (in-render) FPS overlay, in-game options menu.
Done this session (M-25) — 3 commits, port-layer QoL
Free-roam QoL pass, port/ only, zero src/ / game-logic change. Full
review sheet: docs/M-25-QOL-REVIEW.md. All defaults reproduce prior
behavior exactly.
9ebed821—[Video]ge007.ini knobs.Video.VSync/FpsCap/MSAA(1/2/4/8) /TextureFilter(0 nearest, 1 bilinear) /Fullscreen, wired invideo.c videoInit. Plus config auto-migration:config.cnow rewritesge007.inionce when the build registers a key the file never had (so a new section actually appears) — an up-to-date file with a user comment is left untouched.5a7a1035—tools_pc/playtest.shtakes a level name (playtest.sh bunker2) or number,--listprints the 21-level table, and a crash now auto-runsaddr2lineon the faulting PCs.863f436b—[Input]ge007.ini knobs.MouseYScale,MouseSmoothing(0 = off = today),PadDeadzone(7000),PadTriggerPct(23),PadLookInvertY.- Verified per commit: build green,
-level_09crash-free to frame 620 @ 91.6% (unregressed),-level_20crash-free. - Silo
-level_20capture freeze — D117/D134 load sensitivity, NOT a real bug and not an M-25 regression. Full-length captures froze at ~frame 320 (silo→Bond camera descent) twice, but only on a machine still loaded from the sweep (the pre-M-25 baseline froze the same way). On an idle machine the user drove Silo live past ~1800 frames, VI pacemaker healthy, no stall. Seedocs/M-25-QOL-REVIEW.md.
Done this session (M-26) — port-layer QoL batch (A/B/C/E)
Low-risk QoL, port/ only, zero src/ change. All defaults reproduce
prior behaviour. Full review: docs/M-26-QOL-REVIEW.md.
- A — save-on-exit +
[Window]persistence.atexit(portAtExit)inmain.c(videoSaveWindowState()+configSave()), fires on every clean quit (exit(0)invideoPumpEvents);abort()paths skip it. New[Window]ini section (Width/Height/X/Y/Maximized, sentinels = old behaviour) invideo.c;Video.Fullscreennow round-trips. - B —
level_sweep.shSTALLED verdict. Frames rendered then froze with the process still alive →STALLED (froze at N/M frames)instead of a bogus PASS (the Silo case). Retried once like NO-FRAMES. - C — raw mouse input.
Input.MouseRawInput=1(default 0) sets the SDL relative-scale / warp hints off ininputInit— no OS pointer accel. - E —
--help/--versioninmain.cbeforecrashInit(): build id, usage, the 21-level-level_XXtable. - D —
[Debug]ini flags.Debug.FrameDump(mirrorsGE_PCDUMP),Debug.InputLog(mirrorsGE_INPUTLOG); env var wins, ini is the fallback.config.cconfigGetFrameDump()/configGetInputLog(). - Verified: build green (
ntsc-final);-level_09+-level_20boot crash-free 6/6 GE_PCDUMP frames (unregressed);ge007.inigains a populated[Window]block on exit.
Done this session (M-27 continued) — FULL front-end loop works end to end
User playtested Dam → exit → post-mission report → Facility briefing →
Start, exactly like the retail game. No crashes, no hangs. 10 commits
bb6ac627..0fdb14a3 (all committed).
- D142 (
src/bondconstants.h) — GCC/mingw compiles an all-non-negativeenumas UNSIGNED, sofor (s = SP_LEVEL_EGYPT; s >= SP_LEVEL_DAM; s--)infileGetHighestStageDifficultyCompletedForFoldernever terminated → SELECT FILE screen froze the game. Fix:#ifdef PORTnegative sentinel inLEVEL_SOLO_SEQUENCE. PORT-LEARNINGS §D3. - D143 (
src/game/textrelated.c) —langGet()returns NULL for a string slot the PC menu flow hasn't loaded;textRender/textMeasure/ textWrapfaulted on it. NULL guards → blank text instead of crash. (A string bank still isn't loading — briefing/objective text is blank. Cosmetic, chase later.) - D144 / D146 (
port/fast3d/gfx_pc.cpp) — a front-end 3D model (MISSION COMPLETE dossier / mode-select wallets, D75 family) emits a malformed compiled sub-DL (seg5+0x9ee4): unresolved matrix pointer, then garbage opcodes. D144 = bad matrix ptr → identity; D146 = unknown opcode → end the DL (wassysFatalError→abort(), no crash log) +fast3d_ptr_ok()guards ongfx_sp_vertex/movemem/set_vertex_colors. Model renders wrong/absent (GRAPHICS-BACKLOG D149) but the game survives. - D145 (
port/src/video.c,gfx_sdl2.cpp,input.c) — bare ESC wasexit(0)in two event handlers; on the debrief screens ESC is the natural "back" key so paging with it quit the game (clean exit, no log, looked like a crash). ESC now = N64 B (back/cancel); quit = window-X / Alt+F4. - D147 (
port/src/libultra.c) — the end-of-Dam hang.sndPlaySfx→alEvtqPostEventon the main thread races theamMainaudio thread insndRemoveEventson the sameALEventQueue; both "lock" withosSetIntMask(OS_IM_NONE)which was a no-op → list corruption → infinite spin.osSetIntMaskis now a process-wide recursive mutex (OS_IM_NONEacquires,OS_IM_ALLtoken releases). Thread dump confirmed. PORT-LEARNINGS §D4. Verified-level_091200 frames clean.
New test tooling (committed):
tools_pc/debug.ps1—.\tools_pc\debug.ps1 [-level_09] [-Menu 13] [-NoBuild]. Builds, runs under gdb, prints FATAL line + backtrace + crash log on exit →gdb.txt. Use this for every playtest so a crash/hang always leaves a trace.GE_INPUTSCRIPT="<frame>:<tok>,…;…"(port/src/input.c) — headless scripted controller-0 input (buttons pulse,SUP/SDOWN/SLEFT/SRIGHT/ SNONEsustain). Sole input source when set.GE_STARTMENU=<id>(+_PAGE,_DIFF) (src/game/lv.c) — boot straight into a front-end menu (13=MISSION_COMPLETE, 10=BRIEFING, 7=MISSION_SELECT, 12=MISSION_FAILED, 6=MODE_SELECT). Skips a save/unlock so it lands on Dam regardless; good for crash-testing a screen, not for reproducing real objective state.
Still open (cosmetic / not blocking, parked in GRAPHICS-BACKLOG)
- D148 — Dam level-end cutscene (Bond rappelling down the dam) doesn't
play; cuts straight to the report. Scripted-cutscene / cinematic-camera
- rappel anim; D75 family.
- D149 — front-end MISSION COMPLETE / mode-select 3D models garbled or absent (the D144/D146 corrupt DL). D75 family.
- D143 side effect — briefing / objective text renders blank (a lang string bank not loaded in the PC menu flow). Find which bank.
Next
- Continue the mission list: Facility → Runway → … playtest with
tools_pc/debug.ps1, same loop. In-level ABI/GBI crashes are the expected work (D122/D135/D147 pattern). - The D75 front-end / cutscene 3D-model family (D148/D149) is now the biggest visible gap but is cosmetic — below crashes.
Done this session (M-30) — user batch defect list; commits + docs
M-30b — 6-agent parallel burst (2026-08-31), all merged + verified
7 commits 84a8693e..1b69c8ba. Merged tree builds clean; -level_09/-20/-30/-34/-37/-28
render + no crash; menu + Depot visually verified; Agent D's 21/21 with-input
(walk+fire) sweep crash-free. Three texture-pipeline fixes compose correctly.
| Dxx | Fix | Verified |
|---|---|---|
| D158 | KF7 muzzle-flash ((f32*)stackpad2)[-8] negative-stack write → 0xc000001d on fire. Real #ifdef PORT local. |
Caverns survives sustained KF7 fire; stackpad audit found no others |
| D159 | texSwapAltRowBytes (odd-row byte-swap for an N64 RDP TMEM XOR fast3d doesn't emulate) scrambled every ~1:1-viewed texture = the user's "interlaced textures". #ifdef PORT skip. |
menu wallet-photo: comb → clean portrait; -level_09 unregressed |
| D161 | Depot ceiling: 16×16 CI8 tile drawn with TLUT off (G_TT_NONE) → fast3d did a palette lookup on stale palette → blue speckle / radial rays. Decode CI+G_TT_NONE as intensity. |
Depot ceiling: blue garbage → dark industrial roof; -30/09/34 pass |
| D164 | Legal/disclaimer screen drew 1 line — legal_text_end = &legalscreen_MRD assumed linker adjacency (false on mingw). #ifdef PORT bound on the array. |
high-confidence static (nm-verified); needs a boot to eyeball |
| D165 | Front-end mouse cursor: velocity² feel → P-controller 1:1 pointer. Input.MenuPointerMode (def 1), Input.MenuPointerSpeed. |
build + menu smoke; user feel-check pending |
| D166 | Hipfire vertical aim: fixed digital threshold → speed-proportional C-button duty cycle. Input.HipfirePitchSpeed. |
build; user feel-check pending |
| D160 | Dam rappel cutscene (D148) not root-caused — GE_D160=1 diagnostic shipped. Hypothesis: propDef command-index walk desync (D122/D132 family). |
needs: user runs Dam to exit with $env:GE_D160=1, pastes D160: lines |
Also this session (pre-burst): D157 (campaign unlock — user-confirmed), RC2
(Video.FixMipTextures), D120 (PointUsage), D74 (Video.WrapFix opt-in).
M-30 state / next
⚠ Before the next playtest: ./build-pc.sh ntsc-final AND the full 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) — D120 changed
d43_emit.py (PointUsage) and needs the regen; debug.ps1 rebuilds the exe
but does NOT regen sidecars. (Done on this session's machine already.)
Campaign progression + saving works (D157, user-confirmed). The user can
now do the full Track A playthrough — each level boundary exercises the same
save path. GE_UNLOCK_ALL=1 jumps to any level; GE_SAVELOG=1 traces the
save chain if a later level misbehaves.
Fixed + committed this session (all on master, unpushed):
- D165 / D166 (M-31,
port/src/input.c, port-only) — input polish. D165: the front-end cursor is now a true ~1:1 pointer (P-controller estimating the game cursor throughfront.c's own integrator) instead of the M-30 velocity² feel;Input.MenuPointerMode(default 1) falls back to the old mode,Input.MenuPointerSpeedscales it,GE_INPUTLOGtracesmenuptr est/tgt/eff/stick. D166: hipfire mouse pitch is speed-proportional C-button pulses, not a digital threshold (Input.HipfirePitchSpeed). Build green;-level_09+GE_STARTMENU=6smoke crash-free. User feel-check owed (headless can't drive the mouse) — tune MenuPointerSpeed up if the menu sweep feels short. - D157 — campaign never unlocked (objective-difficulty byte read at the wrong offset on LE). Root-caused + user-confirmed. §F D157.
- RC2 (
Video.FixMipTextures, default on) — LOD textures were uploaded ~1.3× too tall; clip to base-tile height. Modest visual win, no regression. - D120 —
d43_emit.pynow emits the opcode-0x18PointUsage[]chain (was zero-filled → blood-decal walk cycled, guarded). Interactive verify pending (BUNKER1 firefight). - Menu #4 — stripped the per-frame D63 log flood that collapsed the
front-end to 5fps (
d0789358). - Menu #2 — front-end mouse-pointer mode (
current_menu != RUN_STAGE→ mouse drives the cursor both axes, LMB=A/RMB=B); Y-invert fixed. - RC4 RETRACTED — the palette decode is correct RGBA5551, the analysis doc's "spec" was ARGB1555. Don't "fix" it.
- 21/21 level sweep PASS (first all-green incl. Cuba) — no regressions.
Still open / next:
- D148 (Dam exit rappel cutscene doesn't play) — user will see it right after Dam. D75 front-end/cutscene-model family, cosmetic, deep. Not started.
- RC1 / wallet-Bond photo garble — D75 family (
texLoadFromGdlmodel-GDL expansion broken for front-end models).TEXTURE-GLITCH-ANALYSIS.md§6b. - Depot ceiling blue-speckle — B2 light-shaft/UV bug on that surface;
RC2 didn't touch it. Needs
GE_DTEX/RenderDoc on the specific surface. - D154 (
bg.cwall-shoot GBI parser, committed072b5c44) — needs a firefight-into-a-wall to verify, then close. - In-level ABI/GBI crashes as the campaign playtest proceeds (D122/D135 pattern) — the expected work.
- Latent:
fileSetDifficultyStageTimetimes[]OOB read for 007-difficulty late levels (retail has it too; edge case, don't fix without growingsave_datawhich breaks EEPROM layout).
BLOCKER (M-30) — campaign progression not persisting — FIXED (D157), CONFIRMED by user playtest
User replayed Dam on Agent post-fix: gdb.txt shows obj 0 objdiff=1 /
obj 1,2 objdiff=2 (skipped on Agent) / obj 3 objdiff=0 status=1 →
objectiveIsAllComplete=1 → end_of_mission_briefing → fileUnlockStage... stage=0 diff=0 → fileWriteSave slot=0 bitflags=10. data/ge007.eep slot 0
now carries the Dam completion time; the Agent checkmark shows in mission-
select and persists across a kill + restart. Campaign progression works.
(GE_SAVELOG diagnostics — #ifdef PORT, env-gated — left in
objective_status.c / boss.c / file.c / file2.c for now; strip once a
few more level boundaries are playtested. dump_objectives.py per-criterion
MinDif= is only valid on the type=23 lines — garbage on 2-word sub-records,
harmless.)
D157 root cause
`D157` (`src/bondtypes.h`, `#ifdef PORT`): the type-23 objective record's `MinDificulty` is a BE `s32` at word 3; `d88_propdefs.py` bswap32's it, so on LE the value moved from byte 0xF to 0xC, but `struct objective_entry.difficulty` still read `s8`@0xF → **0 for every objective**. `get_difficulty_for_objective()` returned Agent(0) for all → `objectiveIsAllComplete()` on Agent evaluated objectives that should be difficulty-gated out (Dam "Neutralize all alarms" = Secret Agent, "Install covert modem" = 00 Agent) → always incomplete → `end_of_mission_briefing()` (the unlock write) never ran. **User was right — you don't shoot an alarm on Agent Dam.** Fixed the struct tail order under `#ifdef PORT`; also fixed `dump_objectives.py` (same byte-offset bug — it showed "Agent" for everything). **Next: user replays Dam on Agent (just reach the exit) under `GE_SAVELOG=1`; expect `MinDif=1/2/2/0` on the crit lines, `objectiveIsAllComplete=1`, a `fileWriteSave slot=N ... TIMES` line, and Facility unlocked in mission-select.** If obj 3 (Agent, flag 0x1000) still shows incomplete, the level isn't setting that stage flag on exit — separate issue. `GE_UNLOCK_ALL=1` remains the playtest workaround meanwhile.Original M-30 blocker writeup (superseded by D157)
User completed Dam, hit "previous" from the debrief to mission select, **Facility still locked**. `data/ge007.eep` has zero completion times → the unlock save isn't landing (or is wiped on reload). Chain: `bossReturnTitleStage` (`objectiveIsAllComplete()` gate) → `end_of_mission_briefing` (`briefingpage`/`selected_difficulty` guard) → `fileUnlockStageInFolderAtDifficulty` → `fileOverwriteSaveSlotWithNewSave` (needs a slot with `SAVEFLAG_DORESET`) → `fileWriteSave` (`fileGamePakProbe`) → EEPROM; then `fileValidateSaves` CRC-checks every slot on menu re-entry and `fileResetSave`s any mismatch. **M-30 GE_SAVELOG run 1 (user's gdb.txt) — narrowed:** `bossReturnTitleStage stage=33` (Dam) fires with **`objectiveIsAllComplete=0`** → `end_of_mission_briefing` never called → no unlock write. `fileValidateSaves` / CRC / DORESET-slot all fine (first-boot fresh-save init, expected). So the blocker is **objective-completion detection**: `objectiveIsAllComplete()` bails at obj 0 (`get_status_of_objective(0) != COMPLETE`). Dam obj 0 = 4× DESTROY_OBJECT (ALARM tags). This is D126/D151 propDef-decode family — either `objective->ObjRefID` at the wrong byte offset on LE (criteria records `_bswap32`'d, `MissionObjectiveRecord.ObjRefID` s32@4 vs the criterion's `u16` tag), or the `sizepropdef` walk stride is off for a criterion type. Note `sizepropdef(PROPDEF_OBJECTIVE_START)` PC-branch returns **4** and `d88_propdefs.py` emits **16 B** for type 23 — but `MissionObjectiveRecord` (bondtypes.h) is header+ObjRefID+TextID+MinDificulty+`*nextentry` = 20 B N64 / 24 B PC. If `nextentry` is real+serialized, both the stride and the converter are 1-2 words short → whole-stream desync. (`struct objective_entry`, the other view of type 23, is only 16 B and has no nextentry — the two structs disagree; the "// maybe wrong..." comment flags it.) **Diagnostic committed (``):** `GE_SAVELOG=1` now also dumps each criterion's `type / ObjRefID / TextID / sizepropdef stride / first 3 words / currentstatus` (gated by `g_savelogObjOnce` so it only fires on the `bossReturnTitleStage` call, not the per-frame poll). **Next: user does ONE genuine full Dam completion (all objectives, walk out the exit) under `GE_SAVELOG=1`; the `SAVELOG: crit ...` lines pin the exact field/stride that's wrong.** Also noted but not chased: `fileSetDifficultyStageTime` `offset = ((diff*20)+levelid)*10` indexes `times[]` up to `[98/99]` but the array is `u8 times[76]` — OOB for high level/difficulty combos (writes into the next slot's checksum). Not the Dam/Agent case (index 0) but a latent save-corruption bug.Done this session (M-30) — user batch defect list; earlier items
User playtest batch report (5 items; #1 disregarded):
- #4 main-menu hang (file-select → main menu → back → 5fps, no input, force-close).
Crash log was D146 spam +
D63 [1479] from=... No stack. Root cause: the D63 debug trail ingfx_pc.cppfired ~10sysLogPrintflines + a full trail walk on EVERY unknown GBI opcode, unconditionally — on the D149 corrupt front-end model DL that ran per-frame and collapsed the menu to ~5fps with input starved. FIXEDd0789358: stripped the D63 trail (statics, per-G_DL recording, default-case dump); the rate-limited D146 "ending DL" log (cap 20) stays. Headless: FILE_SELECT / MODE_SELECT / MISSION_SELECT all boot + render 900+ frames clean, 0-1 D146 lines, no hang.-level_093/3 framediff,-level_2091.6% unregressed. User to re-verify the live menu-nav transition — the exact folder→mode→back path wasn't reproducible headless, but the per-frame log flood (the "5fps no input") is gone. - #2 file-select mouse only moves side-to-side. The front-end cursor is
stick-driven (
frontUpdateControlStickPositionreadsjoyGetStickX/Y); the port only fed mouse-X to the stick in menus (mouse-Y → C-buttons, ignored by menus). FIXED<menu-pointer commit>:port/src/input.c— whencurrent_menu != MENU_RUN_STAGE(and!= MENU_INVALID, so bare-level_XXin-game aim is untouched) mouse velocity drives stick X+Y as a pointer, no C-buttons / aim band, LMB=A (select) RMB=B (back). Reads the game globalcurrent_menufor UI context only (enum MENU is ABI int) — no logic change. New knobInput.MenuPointerSpeed(%, default 100). User to verify feel. - #5 menu + saving for playtest. EEPROM saves (
osEeprom*→data/ge007.eep, write-through, M-29bf5e4d3d) audited — sound; progress persists across restart once the menu works (#4). No change needed; verify after #4. - #3 "interlaced" textures (
screenshots/interlaced texture example.jpg). RC2 (mip contamination) FIXED, default on (<rc2 commit>,port/fast3d/gfx_pc.cpp). NewGE_DTEX=1probe confirmed it: every LOD texture uploaded ~1.3x too tall (64x64 I4 → 64x87), the extra rows being mip bytes rendered as image rows.import_texture()now clips a full-width LOD block to the SETTILESIZE base height; GL builds correct mips from a correct base. KnobVideo.FixMipTextures(=0 → old behaviour, byte-identical to golden). BUNKER1 side-wall textures visibly cleaner with it on; Silo unregressed.-level_09framediff phash on frames 320/440 is D117 noise, not a regression — M-30 verified: BUNKER1's intro camera pans continuously for 1000+ frames and its speed varies run-to-run, so any fixed capture frame lands on a different pan moment each run (a re-baseline attempt to a "settled" 700-1000 window failed the same way — there is no static window in the intro). Trustnonclear(coverage %, stays stable) + the 21-level sweep for-level_09/-level_20; the golden phash frames are only meaningful for gross breakage. Depot M-30 A/B: RC2 makes the wall panels mildly cleaner, does NOT touch the ceiling blue-speckle (separate B2 light-shaft/UV bug), makes nothing worse. 21/21 sweep PASS with the fix on.- Still open: the wallet-Bond photo garble is RC1 / D75 front-end model
family (the model GDL /
texLoadFromGdlexpansion is broken for front-end models — fast3d walks into0xfafbfbfcgarbage, not a missing opcode). Separate larger track — seedocs/TEXTURE-GLITCH-ANALYSIS.md§6b. - RC4 (palette off-by-one,
gfx_pc.cpp:835) and RC3 (wrap period) still open — small, in the same doc §6.
- Still open: the wallet-Bond photo garble is RC1 / D75 front-end model
family (the model GDL /
- Regenerated
docs/LEVEL-OBJECTIVES.mdfor all 21 solo levels (Track A); committed loose reference docs/tools.
Done this session (M-29) — race-to-release plan; audio DEFERRED; 2 commits
Direction (user, M-29): ship the solo campaign start→finish, fastest.
Three tracks: A = full 21-level playtest (release gate, user batch-reports a
defect list from a solo playthrough); B = audio DEFERRED — ship silent,
add sound post-launch (docs/AUDIO-PLAN.md stays valid for later, do NOT
start it); C = solo quick wins, priority: B2 textures > B3 blood-stain
converter > B5/B6 menu input > C2 (D75 models) > C4 (D148 cutscene) >
C1 (mirrored HUD — user unsure it's worth it). Full plan +
docs/SMALL-FIXES.md mapping in the m29-release-push memory.
0bd0ceec— aim sensitivity (SMALL-FIXES B4 / BACKLOG B3):Input.MouseAimSpeeddefault 50→25 (overshot), newInput.AimBandknob (5..40, def 20). Port-only,ge007.ini-tunable. Needs playtest feel check.bf5e4d3d— file-backed EEPROM saves (BACKLOG B5 / Phase 4):osEeprom*inport/src/libultra.cnow backdata/ge007.eep(2 KB / 16Kbit, lazy-load + write-through, PD pattern).osEepromProbe→EEPROM_TYPE_16K. Verified: game writes the file (checksum @blk 0 + save_data @blk 4),-level_09unregressed, persists across runs. Campaign progress now survives quit.- SMALL-FIXES B1 (D74 wrap block) — DEFERRED. The in-place one-liner
(
G_TX_CLAMPtest +[t]index) activates never-run code and boot-crashes-level_09(0 frames). Left inert with a NOTE ingfx_pc.cpp. Needs the hoist-out-of-vertex-loop rework + per-level visual check. - B1 3-point filtering — wired up as opt-in (
a418f4d0), NOT default. The sm64ex 3-point shader path existed but was unreachable + its min-filter row was mip-less. Fixed the row (trilinear), aniso default 0→4, exposedVideo.TextureFilter=2. Default stays 1 (bilinear) — 3-point softens textures at normal distance and (tested) did NOT fix the Depot roof. - B2 (Depot wrong textures) — filtering RULED OUT, still not fixed.
docs/BRIEF-B2-depot-textures.md. The roof renders as blue speckle + radial rays converging to a point; identical with 3-point+trilinear+aniso at 640×480 native, so NOT grazing aliasing. Decode path (TLUT bswap,palette_to_rgba32,import_texture_*) audited clean. It's a texture-data / UV / light-shaft-effect bug on that surface. Next step:GE_DTEXprobe to identify what draws it (params in the brief). This is a RenderDoc/probe-class investigation — D114/D116 rabbit-hole family, needs a focused session, not inline. 7d7f5fb2D155 — Facility outro-cutscene "hang" FIXED. User report.waitForNextFrame()passeddeltaFramesunclamped; on the portosGetCount()is wall-clock (D117) so a real-time stall at the cutscene→debrief asset load (worse with a local LLM thrashing the box) made it balloon to hundreds →g_ClockTimerhuge →modelTickAnimwhile(numticks--)× N chrs + dozens offor(i<g_ClockTimer)sim loops → multi-second frame → heartbeat "hang" + spiral. Fix:#ifdef PORTclamp to 6 inframetiming.c. Likely fixes a whole class of post-slow-load transition hangs — de-risks the Track A campaign playtest (every level boundary loads assets). Confidence high (stack + arithmetic agree).d656823e+b4a7fc2aD156 — Facility outro hang, 2nd occurrence, the ACTUAL fix. After D155 the user re-ran the playthrough → hung again at end of Facility, same stack butframes=12962frozen constant (true infinite loop, not D155's spiral). Thewhile(1)atmodel.c:3131(modelSetAnimFrame2WithChrStuff) steps one anim frame at a time fromframeatoframeb;frameb=modelTickAnim's accumulatedframe. A cutscene anim transition with a near-zero blend/timespeed/unkb0denominator → huge/NaNmodel->speed/playspeed→framebhuge/NaN →floorFloatToIntgarbage → ~2^31 iterations.#ifdef PORTguards: snap a non-finite/|x|>=1e6framebtoframea, and fall backframe/frame2to the pre-loop values inmodelTickAnim. One-shotstderrdiagnostic dumps the model speed fields when it fires. User to re-verify the Facility outro. If it still hangs OR the diagnostic prints, the NaN source is upstream (suspect a D100/D140-style misalignedModelfield, or the cutscene data). §F D155+D156, PORT-LEARNINGS §E.- D153 reminder cost real time this session: back-to-back
-level_09runs during builds boot-crash under machine thrash; only regression-test on a settled machine (clean run = 600+ frames fine).
Done this session (M-28) — D150 (watch page crash) + D151 (watch text blank)
- D150 (
src/str.c,#ifdef PORT, uncommitted) — user found a crash opening level objectives on the watch. AV instrcat(str.c:25, NULL src). The watch BRIEF/OBJECTIVES pages (options.c:3940-4068) build text withstrcat(buf, langGet(id));langGetreturns NULL on PC for the briefing/objective string banks the in-level watch flow never loads (same missing-bank issue as the D143 "briefing text renders blank" note). Fix: NULL-tolerantstrcpy/strncpy/strcatunder#ifdef PORT. Note these are__nonnull__builtins to GCC so a plainif (src==NULL)is optimised away — the guard laundras the ptr through an empty__asm__(GE_IS_NULL). Build green;-level_09600+ frames @ 91.67% unregressed. Watch-page repro is interactive — needs user re-verify. §F/§H D150, PORT-LEARNINGS §C. - D151 (
src/bondtypes.h,#ifdef PORT, uncommitted) — the D150 "objective/briefing text renders blank" consequence, ROOT-CAUSED and fixed. NOT a missing bank: the per-level lang bank IS loaded in-level (langLoadToAddr,prop.c:1274). The propDef records that carry the watch text (types 35WatchMenuObjectiveText, 23ObjectiveStart) store a plains32slot id in their 3rd word;struct watchMenuObjectiveText/struct objective_entrydecode it asu16 reserved; u16 text;and readtextat offset0xA— works only because on BE the id is in the low 16 bits.d88_propdefs.py_bswap32's the whole word →text@0xA reads 0 →langGet(0)→ NULL → blank. Fix:textis a fullu32at the word offset under#ifdef PORT(N64u16 reserved; u16 text;kept under#else). No converter change / no sidecar regen. Compiles clean (link needs the running game closed). Interactive re-verify pending — open the watch, all of OBJECTIVES / mission background / M / Q / Moneypenny should now show text. §F D151, PORT-LEARNINGS §C.
D152 (OPEN) — mission-failed → permanent black screen = osSetIntMask lock deadlock
User killed Trevelyan in Facility (Ctrl = fire, no crouch bind) → failed
the objective → fade to black, never returns; process alive. Live gdb.txt
inspected (game left running under debug.ps1): NOT the front-end
mission-failed screen — it's a deadlock on s_imLock (the D147
recursive-mutex behind osSetIntMask). Rendering froze at frame 12600.
mainThread in sndSetScalerApplyVolumeAllSfxSlot→alEvtqPostEvent→
osSetIntMask(OS_IM_NONE)→pthread_mutex_lock (waiting); amMain in
sndPlayerVoiceHandler→alEvtqNextEvent→same, also waiting; neither owns the
lock → a leaked unbalanced OS_IM_NONE (libaudio early-return paths) or a
transient thread that acquired-and-exited holds it. The mission-failed audio
fade-out (sndSetScalerApplyVolumeAllSfxSlot posts AL_SNDP_RELEASE_EVT per
ALSoundState per frame) is what exercises the window. D147 family; D147's
"cannot deadlock" claim is now falsified. §F D152, PORT-LEARNINGS §D4.
MITIGATED (M-28, port/src/libultra.c, port-only, uncommitted).
osSetIntMask is now a self-healing logical lock: same recursive
acquire/release as the D147 mutex on the normal (microsecond) path, but a
waiter blocked > 2 s (OS_IM_STUCK_NS) declares the holder leaked it,
logs LOG_ERROR with the stale owner + the stealing caller's return
address, and steals the section. A leaked/unbalanced OS_IM_NONE (or an
acquire-and-exit transient thread) can no longer wedge the game forever —
worst case is a ~2 s audio hiccup + a log line that names the leak. Builds
clean. Still OPEN: the exact leaking call site (read it from the next
D152: ... stealing from owner= log) + the proper narrow fix (dedicated
ALEventQueue lock). Next: replay the Facility mission-fail; if the steal
fires, addr2line the logged caller.
Separate minor gap: no crouch keybind (BACKLOG B3/B4).
D154 (WRITTEN, UNVERIFIED, UNCOMMITTED) — bg.c room hit-test GBI parser port
bgTestRayIntersectionInRoom (src/game/bg.c:3331) is the D135 sibling the
docs kept flagging: an unported N64 GBI parser (((u8*)gdl)[k] / ((u32*)gdl)[i]
byte/word indexing) walking the 16-byte PC room DL as if it were 8-byte N64
Gfx. Fires on shooting walls/floor (bgTestBulletHitBackground), which no
level-sweep exercises. Ported under #ifdef PORT (N64 path verbatim under
#else): header fields via the .dma view like bgBuildRoomVtxBounds; G_TRI1
- G_TRI4 vertex-index nibbles recovered from
(u32)gdl->words.w0/.w1(derivation table cross-checked twice);texturenum-> -1 like D135 (KSEG0 deref invalid for converted GDLs; only flavours the impact decal/sound, D77). Builds clean. NOT verified — a no-input capture never calls this function, so it needs a real firefight into a wall on an idle machine; couldn't get a clean run this session (D153/driver-crash under machine thrash — see below). Next session: build,-level_09, fire at a wall, confirm no crash +-level_09framediff green, then commit. §F D154.
D153 — -level_09 frame ~900 crash = load flakiness, NOT a bug (closed)
Chased for ~30 min in M-28: -level_09 crashed 0xc0000005 at a
nondeterministic frame 900↔1400, 6/6 runs — but every one was while the
machine was hammered with concurrent builds + multi-run loops. On a quiet
machine -level_09 ran past frame 2400, 0 heartbeats, no crash. It's the
D117/D134 host-scheduling flakiness the docs warn about every session (M-25
Silo ~frame 300 is the same). Not a regression, not D150/D151/D152.
romdataFixupMusicSeqTable: seqCount 63 exceeds blob capacity 1 is a benign
expected warning (header-only first call). Lesson: never regression-test
while builds/other game runs are in flight. §F D153 (closed).
M-27 earlier — front-end/pause bucket: D140 + D141 FIXED
Started the parked "front-end / transitions / pause / watch" bucket.
Pressing Start in-level no longer crashes — the pause / watch menu
renders (weapon page verified via a temp GE_AUTOPAUSE input probe,
since removed). Cosmetics unchanged and still parked: mirrored watch text
(D114/D116), dark/faint weapon model (D75).
- D140 (FIXED)
src/game/bondview.h,src/game/bondview2.c— pause-menu crashbondviewRenderWatch→bondviewTransformManyPos- ToViewMatrix(field_23C=NULL).something_with_watch_object_instance(N64 player +0x230) is astruct Model+ RW-pool punned into a 0x184-byte field-run;field_23C/watch_scale_destination/pause_watch_related_adjustare actually.render_pos/.scale/.animframe1. PCsizeof(struct Model)grows → the aliases break → NULLrender_pos. Fix: real inlinestruct Model+u32 watchRwPool[192]under#ifdef PORT; the 3 named reads redirect to the member viaGE_WATCH_{ANIMFRAME,SCALE,RENDERPOS}macros (N64#else= verbatim field). D56 branch ofsub_GAME_7F07E7CCnow uses the inline pool. D100/D102 pattern. §F/§H D140, PORT-LEARNINGS §A. - D118d (FIXED, feel unconfirmed)
src/game/options.c— user bug report: watch inventory list over-scrolls with keyboard W/S (one press skips several items). GE's "slam the stick" fast-scroll is a raw per-frame level check (joyGetStickY < -0x46); keyboard/digital pads sit at max every frame.#ifdef PORTdrops the stick term → list-nav goes through the latched single-step path (one step per press). Build green,-level_09unregressed; couldn't verify the feel headlessly (INVENTORY watch page unreachable without page-cycling input) — needs an in-game check. Latent sibling infront.cmenu nav (samejoyGetStick*InRangelevel-check pattern). §F D118d. - D141 (FIXED)
src/game/gunfire.c— the crash D140 exposed.set_enviro_fog_for_items_in_solo_watch_menuwalksbodymodel->Switches[](aModelNode*array) with raw byte offsets+0x48/+0x5c,j += 4(4-byte-pointer constants) → PC 8-byte stride reads garbage → bogus node → AV inmodelGetNodeRwData. Fix:#ifdef PORTusesSwitches[18 + (j>>2)]/Switches[23 + (j>>2)]. §F D141, PORT-LEARNINGS §B (cf. D128). - Verified: build green (
ntsc-final);-level_09framediff 3/3,-level_20crash-free (both unregressed); autopause probe → watch page renders 400+ frames, noge007.crash.log. - D139 (stage-unload, M-23, still UNVERIFIED) not exercised — it needs a real level-exit teardown, not a pause. Next front-end item.
- Still in the bucket: D139 verify; unpause / watch-page navigation (the temp probe didn't test exiting the watch); level exit → MISSION COMPLETE → debrief → auto-advance; main-menu / mission-select walkthrough (WS2); watch objectives page + gadgets.
Update (M-27 end): the level-exit → MISSION COMPLETE → debrief → auto-advance → next briefing → Start loop is DONE and playtested (D142– D147). D139 got exercised for free. Remaining bucket items (unpause / watch-page nav) are minor.
M-24 "Next task" (superseded — this is M-50 narrative archive, not live work; §H authoritative)
Scope call (user, M-23): D139 (stage unload) + D140 (pause menu / watch)
are the same not-yet-built front-end/transition layer — level exit →
debrief → next briefing, pause, the watch. Bare -level_XX skips all of it
and the sweep never touches it. Do not build a throwaway exit path just
to verify D139 — its fix is unambiguous (LE reads the wrong header byte) and
committed; it'll be exercised for free once that layer is built. Both go in
a parked "front-end / transitions / pause / watch" bucket for one
dedicated session later (see docs/PLAN-linear-level-sweep.md WS2).
M-24 = keep doing in-level playtesting. In-level crashes (D135/D137/D138
type — pointer-width / ABI / GBI-parser bugs) are the real work and show up
during normal -level_XX play.
- Resume WS6
docs/LEVEL-PLAYTEST.md, bare-level_XX. Do the in-level ~80% (spawn, geometry, guards, doors, lifts, switches, pickups, each difficulty-gated objective registers COMPLETE viaobjective_status, alarms/reinforcements). Defer the exit-trigger / auto-advance / pause / watch checks — those need the parked front-end bucket. Facility first (it's playable start→~end now), then down the mission list. - Each new in-level crash:
tools_pc/repro_gdb.sh <XX>, get the realbt full, expect another#ifdef PORTABI/layout fix (D122/D126/D132/D135/D137/D139 pattern). Log as the next Dxx. - Re-run
tools_pc/level_sweep.shonce — D135/D138 touch shared code paths; sweep is boot-only so it only proves no boot regression. Refreshdocs/LEVEL-STATUS.md. bg.chit-test sibling port (§F D135 follow-up — identical unported GBI parser to D135,~3373-3646) — do it before a level where wall-shooting is heavy (i.e. soon).
Parked bucket — front-end / transitions (STARTED M-27):
D140 (pause menu →FIXED M-27 (+ D141, the watch-page crash it exposed). Pause now renders.bondviewRenderWatch→ NULLfield_23C)- D139 verify (stage unload /
cleanupObjects) — still needs a real exit - unpause / watch-page navigation (M-27 probe only tested opening it)
- level exit → MISSION COMPLETE → debrief → auto-advance to next briefing
- main menu / mission-select / difficulty-select playtest (WS2)
- the watch (objectives page, gadgets) — needed for the WS6 exit/objective checks, D75 3D-model-transform family
Verification per fix: target level boots to ≥1 non-degenerate frame
(tools_pc/pixcount.py), AND -level_09 + -level_20 unregressed
(tools_pc/framediff.py). Regen sidecars after any converter change.
M-22 and earlier "Next task" (superseded — §H authoritative)
-1. D134 DONE (M-22) — the frame-2 boot hang that every session since
M-13 wrote off as "sweep flakiness / D117 / machine load" was a real,
fixed bug: the SP/DP task-done event was posted OS_MESG_NOBLOCK into
the sched interruptQ that the 60 Hz VI pacemaker also fills, so a slow
synchronous fast3d frame let a retrace backlog swallow the done event →
permanent stall. Fix in port/src/libultra.c (portPostEventForce +
2 reserved slots in portPostVIEvent). -level_09 6/6 boots to frame 600,
0 heartbeats (pre-fix 1/3). §F/§H D134, PORT-LEARNINGS §E.
-
D132 DONE (M-21, commit
fa296b17) — propDef union-index slots for types 14/19/38/44 (LINK/SWITCH/LOCK_DOOR/SAFE_ITEM) now emitted at the right PC offsets (d88_propdefs.py+loadobjectmodel.c sizepropdefPORT). -
-level_09(BUNKER1) boot crash — DISPROVEN as a regression (M-20, coordinator: 3 runs / 1800 frames crash-free on1fc3cff6; re-confirmed M-20 this pass: 690+ frames at 91.7% coverage). Was sweep flakiness / D117 nondeterminism under machine load. Do not investigate. -
Intro "renders mostly black" is NOT a regression — it is the known D75/D76 parked-cosmetic steady state (M-20 / D133). Verified by building M-17 (
9ec6121e, whose handoff claimed "the entire intro renders") in a scratch worktree and capturing the sameGE_PCDUMPwindow: coverage is pixel-identical to HEAD (legal screen 6677 non-clear px = 2.17% both builds; logo-ish frames ~7%; black gaps in between). The 2D/text layers draw; the animated character-model layers (Nintendo-logo transform, gun-barrel Bond, cast models) never appear — exactly D75. The M-17 "entire intro renders" handoff line was aspirational, not a measured state. Parked below level/crash work (docs/GRAPHICS-BACKLOG.md). Silo (#3, fly-down hang) is the same class or D117 nondeterminism — not a fresh regression. -
Facility
-level_34+ Jungle-level_37re-verify FAILED this pass (M-20, machine lightly loaded). Facility: boot crashframes=0, PC0x1400c3b77(addr2line unresolved). Jungle: renders frames 1–2 then kernel-heartbeat hang ("no frame rendered",frames=2). Both were claimed PASS in M-18/M-19 (D130/D131). Needs a clean-machine re-verify before deciding regression vs. flakiness — seedocs/LEVEL-STATUS.md.-level_09/-level_20still PASS, so the capture harness is sound.
After that: all 21 load+render → hand docs/LEVEL-PLAYTEST.md to the
user for the WS6 completion pass (real input, per-level objective
checklist).
Regen gotcha: d69_emit.py rewrites data/pccg-<r>/pccg.bin from
scratch with only the 52 bg/stan rows — you MUST follow it with
d88_emit.py --regen to re-add the 21 Usetup*Z rows, or every level
falls back to the raw N64 setup and crashes identically in
proplvreset2. Always run the full d43 && d69 && d88 --regen chain.
Sweep note: level_sweep.sh was flaky this session (spurious NO-FRAMES on
known-good levels under machine load / the 24 s watchdog + D117
nondeterminism). Verify individual levels with a 35–45 s window and
GE_PCDUMP="60-500:40" when a sweep row looks wrong.
Sweep runner: tools_pc/level_sweep.sh (bare -level_XX, GE_PCDUMP="80-260:40",
24 s watchdog, taskkill //F //IM ge007.x86_64.exe; export PATH=".../mingw64/bin:$PATH"
before addr2line — the script's own symbolication no-op'd without it).
The M-13 matrix below is partially superseded by M-14 (see §H D125 for the current 12/21 status) — treat §H as authoritative.
| Class | Levels | Site |
|---|---|---|
| C1 | Dam 33, Runway 35, Frigate 26, Statue 22, Streets 29, Cradle 41 | chrIsNotDeadOrShot chraction.c:4483 — self = AI-list rodata ptr |
| C2 | Facility 34, Jungle 37 | import_texture_i8/gfx_tex_normalize_source — bad tex ptr |
| C3 | Aztec 28, Bunker2 27 | propobj.c door model / linkedDoor walk |
| C4 | Depot 30 | prop.c:902 sp4C->room after walkTilesBetweenPoints |
| C5 | Control 23 | bg.c:5723 portal_pts->numPoints |
| C6 | Surface2 43 | loadobjectmodel.c:393 PitemZ_entries[modelid].header (D122 cont.) |
| C7 | Surface1 36 | snd.c:653 sndSetupSound (audio — parked subsystem) |
Done M-13: C1 fixed (D123, tools_pc/d88_propdefs.py — the C1
crash was D122 fallout: widened Vehichle/AircraftRecord.ailist slot
zeroed instead of carrying its read-before-write int id). C2 split:
Jungle's texture crash fixed (D124, port/src/gimgfixup.c — compiled
explosion-DL sync keyed on an already-erased marker); Facility+Runway
diagnosed (model-GDL relocation misaligns dst, objecthandler_2.c /
texLoadFromGdl, D80/D82/D83 area) — NOT fixed.
Full re-sweep after both: 12 / 21 PASS (docs/LEVEL-STATUS.md).
9 crashes remain in 6 classes: C2 Runway+Facility (model-GDL align —
biggest), C3 Aztec+Bunker2 (docs/BRIEF-C3-C6-prop-model.md),
C2m Jungle (explosion-DL G_MTX), C6 Surface2 (PitemZ modelid),
C5 Control (BG portal), C4 Depot (BG tile/room), C7 Surface1
(sndSetupSound). Priority order + files in docs/LEVEL-STATUS.md "Next".
Uncommitted M-13 edits: tools_pc/d88_propdefs.py, port/src/gimgfixup.c,
§F/§H D123+D124, docs/LEVEL-STATUS.md, docs/LEVEL-PLAYTEST.md,
docs/PORT-LEARNINGS.md, tools_pc/level_sweep.sh, the BRIEF-C* files,
this file. Fold into the docs-restructure commit set.
Re-run tools_pc/level_sweep.sh after each fix; keep -level_09 +
-level_20 green (framediff.py). All-21-PASS → hand the user
docs/LEVEL-PLAYTEST.md for WS6.
Verification per fix: target level boots to ≥1 non-degenerate frame
(tools_pc/pixcount.py > a few %), AND -level_09 + -level_20
unregressed (tools_pc/framediff.py). Regen sidecars after any converter
change: 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.
Parked (do NOT start here): WS2 front-end menu playtest (needs a
human driving; cosmetics D75/D76/D77/D114/D116 all out of scope —
GRAPHICS-BACKLOG.md). Audio (Phase 3), saves (Phase 4).
Environment / build
export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final # ~5 s
Run from the repo root, not build-pc/.
- Repro (BUNKER1, skips attract mode):
./build-pc/ge007.x86_64.exe -level_09(per-level-m*pool sizes are now auto-injected — D121/WS1; pass them by hand only to override).boss.c:337decodes-level_XXasd0*10 + d1 - 0x210; LEVELID == the-level_XXnumber. - Sidecar regen (after any converter change;
data/is gitignored):Ifpython tools_pc/d43_emit.py ntsc-final && \ python tools_pc/d69_emit.py ntsc-final && \ python tools_pc/d88_emit.py ntsc-final --regendata/is missing:cp baserom.u.z64 data/ge007.ntsc-final.z64first (see §F "data/deletion + recovery"). - Frame capture:
GE_PCDUMP="<start>-<end>:<stride>"→./ppm/(gitignored). Analyse withtools_pc/pixcount.py(non-black content check) andtools_pc/framediff.py <ppmdir>(structural regression vstools_pc/golden/— the port is NOT frame-deterministic, D117; use--maskfor the HUD, not an exact compare).tools_pc/ppm2bmp.pyto view. - Crashes:
ge007.crash.log(repo root). Symbolicateaddr2line -e build-pc/ge007.x86_64.exe -f -C <pc>(image base0x140000000). Frames past the true chain may be stale (D56). - gdb: launch mode is too slow for timing-dependent faults. Attach
mode is fast:
gdb -batch -x cmds.txt -p <winpid>(<winpid>= 4th column ofps -p <bashpid>; game must already be running, e.g.nohup … &). A hardware watchpoint on a global catches a bad write in < 1 min. - Standalone probe compiles need
-std=c11.
Non-negotiables (full list in AGENTS.md)
- N64 build files (
Makefile,tools/,rsp/,ld/) untouched. - Game logic unmodified except narrow, documented
#ifdef PORTABI/layout/format exceptions — each logged as a Dxx in §F/§H with the N64 line kept verbatim under#else. Anything beyond a narrow, obviously-correct exception: stop and write it up with a confidence rating, don't hack it in. - Prefer an offline sidecar converter (
tools_pc/d*_emit.py, D43/D69/D88 pattern) over a runtime fixup for a whole ROM-asset format.