- Commit CLAUDE.md + AGENTS.md so a fresh clone has the operating manual and
the ABI/no-game-logic rules (trimmed of stale phase status; the personal
pd_port path removed; points at the split docs).
- Sweep every stale doc reference across the tree after the restructure:
PCPortResearch.md -> internals.md / dev/findings.md,
PORT-LEARNINGS.md -> porting-notes.md,
AGENT-WORKFLOW.md -> dev-process.md,
docs/{GRAPHICS-BACKLOG,HANDOFF-ARCHIVE,GE-ENV-PROBES,LEVEL-*,TEXTURE-GLITCH-ANALYSIS,AUDIT-M6-*,AUDIO-PLAN}.md -> docs/dev/.
- Drop the superseded docs/PLAN-linear-level-sweep.md.
- docs/HANDOFF.md + docs/BACKLOG.md stay local-only (rolling working files);
CLAUDE.md now handles their absence in a fresh clone.
Front-end mouse pointer can't reach the outer mission-select grid cells
(only an inner ~3x3). The D165 P-controller in port/src/input.c hard-codes
a 320x240 virtual field; GE's front end runs 440x330, so the pointer
target/estimate saturate at 300/220 before the grid's outer split points
(317 / 235.5). File-select/mode/main-menu unaffected (targets inside the
320x240 sub-box). Fix proposed (derive clamp bounds from
getPlayer_c_screenwidth/height, seed estimator from the real cursor) but
NOT applied - no build in the migrated tree to verify link + regression.
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
gfx_opengl_dump_bound_fbo wrote glReadPixels output (GL bottom-left
origin) straight into a top-row-first P6 PPM, so every GE_PCDUMP / F12
capture was upside-down. This is the entire source of the D114/D116
"HUD/text X-mirror" - four sessions chasing a fast3d mirror that never
existed, misreading inverted screenshots of asymmetric content. The
developer confirms the game renders correctly on real hardware.
- writer now emits rows top-to-bottom
- tools_pc/golden/*.png flipped in place to match + golden/README.md
- D114/D116 reclassified as capture-orientation artifacts (not deleted);
D75 "logo mispositioned" / RC1 "photo 180 deg" flagged for re-judging
- annotations in findings.md, GRAPHICS-BACKLOG.md,
TEXTURE-GLITCH-ANALYSIS.md, porting-notes.md D2, GE-ENV-PROBES.md
Not runtime-verified (migrated tree has no ROM/toolchain) - needs a
fresh capture to confirm text reads normally.
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
- README: "not playable yet" callout under the screenshots (no audio,
broken front-end models/text, expect crashes past the level intro).
- README/building.md/ci.yml: platform testing is Windows-only. The Linux
build is provided but has had zero functional testing at runtime; the
CI job is a CMake-configure smoke test, not a Linux-support claim.
Claude-Session: https://claude.ai/code/session_01UpTM1AcBRJuTetoZNcCD5V
- README, LICENSE (MIT for the port layer), NOTICE, CONTRIBUTING.
- Restructure docs: internals.md, porting-notes.md, dev-process.md +
dev/agentic-development.md (the research write-up), dev/ (raw record).
- Replace decomp status-badge CI with a PC-port validate workflow.
- Remove session/agent scaffolding and stale planning docs from the tree.
- Add .gitattributes (normalize line endings to LF); add screenshots.
The pre-2026-08 history is the GoldenEye 007 decompilation by Larry Ficken
(KholdFuzion) and contributors, preserved unchanged. The PC-port work from
2026-08-17 on is mine.
Adds env-gated GE_D75 probe in title.c sub_GAME_7F007F30 (capped 8 lines).
Booted the bare front end through the gun-barrel sequence:
- Bond chr model + gun model entirely absent; both Model instances are
valid (non-NULL obj, numMatrices 21/1) -> 'model not loaded' ruled out.
- chrModelInstance->render_pos is valid and freshly written each frame
(== that frame's dynAllocate mtxlist base, clean double-buffer swap,
o2p returns it unchanged) -> the 'render_pos = stale dynAllocate arena'
hypothesis (D115 #5) is RULED OUT; do NOT land a persistent render_pos
buffer.
- Zero fast3d DL warnings during the sequence -> not the D144/D146
corrupt-sub-DL family.
Failure is downstream in drawjointlist/dotube vtx/node-DL resolution or an
off-screen basemtx. Next step: a drawjointlist-level probe. Full write-up
in PCPortResearch.md 'D75 Bug 2 - RUNTIME PROBE'.
-level_09 nonclear stable (91.66->91.62, D117 pan noise); build green.
Removes inert env-gated (GE_D54/GE_D63/GE_D90) probe blocks left in game
and libaudio TUs after those findings closed. All were #ifdef PORT +
getenv-guarded with no side effects; bg.c also drops a bare non-PORT
static (d63bgprimarycount) + entry log. No behaviour change.
Build green (ntsc-final, 10/10 link); -level_09 + -level_20 render
crash-free (19 frames each, Silo frame 300 @ 88.4% non-clear).
Static-only pass. (a) D73 scope gap DISPROVEN: src/libultra/gu/* and
matrixmath.c are all endian-clean; the Rareware logo exercises the full
gu pipeline front-end and renders correctly.
D75 now splits into two independent bugs:
- Bug 1 (Nintendo/GE logo transform, wallet/Bond-photo 180deg): the
parked D114/D116 shared fast3d viewport/MP-matrix mirror. Not game
code; static tracing banned per PORT-LEARNINGS D2.
- Bug 2 (gun-barrel Bond + cast models entirely absent): category (b),
independent of any transform. In-level skeletal models render via the
identical joint path, so the discriminator is the front-end setup:
render_pos pointed at a transient dynAllocate gfx arena (D115 MED #5)
is the lead suspect and an in-scope #ifdef PORT fix candidate.
docs only: PCPortResearch.md D75 ADDENDUM + index row; PORT-LEARNINGS
section C (D73/gu scope now settled - stop re-auditing).
fast3d never stored the N64 tile mask and wrapped textures purely at the
uploaded image size; the RDP wraps a WRAP render tile at 1<<mask, which GE
sets to ceil(log2(dim)) - so non-power-of-two surfaces (Depot room
textures) repeat at the next power of two on console and one image-width
too soon in the port ('textures repeat oddly').
- gfx_dp_set_tile: store masks/maskt on the tile.
- gfx_sp_tri1 hoisted pre-wrap block (the reworked D74 block, indexed by
texunit): when WRAP and 1<<mask != tex_width, fold UV at 1<<mask and
clamp the [dim,1<<mask) overflow band (no real texels) to the edge.
- GE_WRAPFIX env override (wins over the ini) for testing.
Behind the existing Video.WrapFix knob, default OFF -> byte-identical to
golden. Per-level captures (-level_09/-30/-34/-20, off vs on): no crashes,
no structural regression; Depot shows small localized ceiling/wall texel
changes. Kept default off pending a human visual check on Depot.
docs: PCPortResearch.md RC3/D167, PORT-LEARNINGS.md D, TEXTURE-GLITCH-ANALYSIS.md 6d.
Static audit of every osSetIntMask(OS_IM_NONE) in the compiled audio code
(event.c, csplayer.c, synaddplayer.c, snd.c): all balanced on every exit
path, including alEvtqPostEvent's freeList-empty early return. No unbalanced
same-thread leak exists -- the D152 s_imLock wedge is not a missing restore.
Two real defects fixed (#ifdef PORT, N64 verbatim under #else):
1. snd.c sndSetSfxSlotVolume walked the live ALSoundState list and posted to
the shared ALEventQueue without holding OS_IM_NONE, unlike its twin
sndDeactivateAllSfxByFlag. On PC: unguarded walk racing amMain + one lock
acquire/release per matching sound. During the mission-failed fade-out
(sndSetScalerApplyVolumeAllSfxSlot -> sndApplyVolumeAllSfxSlot -> here,
per frame) that is a per-frame lock-acquire storm. Now holds the mask once
across the walk; sndApplyVolumeAllSfxSlot batches its slot loop under one
recursive hold.
2. port/src/libultra.c: portThreadWrapper now calls imThreadExitRelease() when
a host thread returns from its entry -- if it still owns s_imHeld, release
the orphan (+ LOG_ERROR). Kills the 'transient thread acquired OS_IM_NONE
and exited' leak (the §F dump's own alternate hypothesis) and the re-wedge
when a new thread reuses the dead pthread id and imAcquire mis-detects
recursion. Steal-lock kept as backstop.
Build 240/240 green; -level_09 frame 300 renders 91.7% non-clear crash-free;
-level_20 boots crash-free. Fade-out repro is playtest-gated (not headless-
reproducible) -- user to confirm no black screen + no D152: log lines.
Re-audited the M-28 bgTestRayIntersectionInRoom GBI-parser port (bg.c):
- G_TRI1 (w1>>16/8/0 & 0xff /10) and all 12 G_TRI4 nibble recoveries
cross-checked twice against the N64 #else path and the raw BE-word bit
layout -- both CORRECT, no change.
- BUG: vtxoff read gdl->dma.par & 0xf. The PC Gdma_le shim maps .par to
bits 0-23 of word0 (packed length, 16-aligned) not the N64 byte-1
params field at bits 16-23, so vtxoff was silently 0 -> wrong
vtxbase[idx] for any G_VTX batch with v0 != 0. Now
((u32)gdl->words.w0 >> 16) & 0xf.
- Sibling ported: bgTestBulletHitBackground tail (post-hit G_SETTILE
back-scan for tileformat/tilesize) was itself an unported raw
((u8*)gdl)[0]/[1] + 64-bit words.w0<<11>>30 parser over the same
D85-widened 16-byte room DL. #ifdef PORT: opcode from bits 24-31,
byte1 from bits 16-23, (u32)-forced 32-bit width for the tilesize
shift. N64 path kept under #else.
- Diagnostic: GE_D154=1 logs the parsed cmd / vtx-index stream (call
header + per-TRI1/TRI4 idx) for the first 64 invocations so the
integrator/user can eyeball the parse. Verification is playtest-gated
(shoot a wall/floor on an idle machine) -- the no-input level sweep
never enters this function.
Build green (ntsc-final, 242/242). ABI/GBI-parse only, no game-logic
change. docs/PCPortResearch.md D154 + PORT-LEARNINGS §B updated.
port/src/input.c only (port-layer, no game-code change):
D165 - the M-30 menu 'pointer mode' fed mouse velocity into the stick, but
front.c frontUpdateControlStickPosition integrates the stick as a velocity
into a screen-pixel cursor -> velocity^2 feel. Replace with a P-controller:
maintain menuEst{H,V} (our estimate of the game cursor, stepped each poll with
front.c's exact recurrence via menuCursorStep), accumulate menuTgt{H,V} from
raw mouse motion, emit stick = clamp(6.0*(target-est), +/-70). Gain 6.0 =>
per-poll error decay 0.45, no overshoot, ~8-poll settle, parks under the
game deadzone with no drift. Input.MenuPointerMode (default 1) keeps the old
velocity mode as a fallback; Input.MenuPointerSpeed scales the mapping;
GE_INPUTLOG prints an est/tgt/eff/stick trace. No mouse motion => emitted
stick unchanged => prior behaviour exactly.
D166 - hipfire mouse pitch was a fixed digital threshold while yaw is analog.
Emit the C-button as a duty-cycled pulse whose frequency scales with mouse-Y
speed (fast = solid hold as before, slow = sparse taps). Input.HipfirePitchSpeed
tunes it. Aim-mode (RMB, already analog) untouched.
Build green; -level_09 (~360 frames) + GE_STARTMENU=6 smoke crash-free.
Feel-check owed (headless can't drive the mouse).
The Depot roof (CI8 tile drawn with gsDPSetTextureLUT(G_TT_NONE)) was
decoded against a stale rdp.palette, producing blue/magenta noise that
aliased into radial rays on the receding ceiling plane. A CI-siz tile
with the TLUT disabled must be treated as raw intensity (I). Route
CI4/CI8 -> I4/I8 in import_texture() when palette_fmt == G_TT_NONE.
Adds GE_TEXDUMP (env-gated PPM + param dump) as an inert diagnostic.
-level_09/-30/-34 sweep PASS, unregressed.
D159 (RC1 / D149): texSwapAltRowBytes pre-swaps odd texture rows in u32
groups to cancel the N64 RDP odd-line TMEM address XOR during word block
loads. fast3d emulates no such XOR, so odd rows uploaded scrambled -> the
'interlaced' wallet-Bond photo and other large front-end I4/I8/IA8
textures. #ifdef PORT early-return; N64 body kept verbatim. Wallet photo
now renders as a clean grayscale portrait (still 180deg-rotated - D75
model-transform family, parked). -level_09 frame 200 coverage unregressed
(91.62 vs 91.66 golden = D117 noise), visual frame clean.
D164 (D76): constructor_menu00_legalscreen bounded its text loop on
&legalscreen_MRD assuming linker adjacency to legalpage_text_array;
mingw reorders file-scope globals (MRD lands 0x60 before the array) so
the do/while ran once. #ifdef PORT bounds on the array's own length.
Dam exit should play a scripted cutscene (Bond off the dam / bungee) before
the MISSION COMPLETE report; on PC it cuts straight to the report while
progression still works (bossReturnTitleStage fires). Add GE_D160=1 trace
points to localise whether the cutscene AI runs:
- bossReturnTitleStage entry (+ caller return addr)
- bondviewSetCameraMode (mode, stage, g_IntroSwirl, intro anim idx, caller)
- AI EndLevel/exit_level, CameraLookAtBondFromPad, CameraSwitch (tag lookup
result: cdef ptr / TagIndex<0 / tag NOT FOUND)
No behaviour change (all #ifdef PORT, getenv-gated).
gunUpdateAndFire's KF7 branch (`mdlhdr->Skeleton == &skeleton_gun_kf7`)
does `((f32*)stackpad2)[-8] = ...` then reads it back into
matrix_scalar_multiply -- a decomp register-allocation stackpad: a
scratch f32 the N64 compiler placed 32 bytes below the `u8 stackpad2[4]`
local. GCC's x86-64 stack layout is different, so the negative index
scribbles into an unrelated stack slot (return-address / saved-reg area)
-> 0xc000001d illegal-instruction the moment you fire a KF7.
Found via scripted-input (walk+fire) testing: 12 campaign levels survived
a firefight script, Caverns (-level_39) crashed reproducibly on a settled
machine at gunfire.c:817. addr2line -> gunUpdateAndFire. The KF7 is a
common campaign weapon (Facility/Surface/Caverns guards) so this would
hit the user early.
Fix: real `f32 kf7_flashscale` local under #ifdef PORT; N64 stackpad
expression kept under #else. Caverns now survives repeated fire to frame
1500+; -level_09 unregressed. PORT-LEARNINGS §A (stackpad hazard).
The D74 block in gfx_sp_tri was inert (guard `cms & G_TX_WRAP` == `& 0`,
always false) AND OOB (`tex_width2[i]` indexed by the vertex i=0..2, but
the array is per-texunit [2]). SMALL-FIXES B1: the naive in-place
correction boot-crashed -level_09.
Rework per the B1 plan: precompute per-texunit `wrap_s/t[t]`,
`wrap_tw/th[t]`, `wrap_uls/ult[t]` once in the texunit setup loop (needs
the original cms/cmt before the CLAMP-clear), keyed on 'clamp bit not
set' + window smaller than the uploaded image; apply per vertex indexed
by `t`. Gated behind `Video.WrapFix` (default 0 -- new/never-run path).
Verified: WrapFix=0 unchanged; WrapFix=1 -level_09 renders clean through
frame 375, no boot crash, no visible corruption. Visual fix on wrapped
triangle textures (room panels) needs a level with them + an N64
reference -- Depot per B1.
Confirms the D120 fix converter-side: 1469 PointUsage regions / 70177
s16 entries across the model set (every guard/character has several) now
byteswap-round-trip, every index < numVertices, and no region is
all-zero (the pre-M-30 bug). Guards against a future emit-gap
regression.
Drop the verbose raw-words / DESTROY obj-ptr-healthy lines now that D157
is root-caused + confirmed. Keep the compact per-criterion line
(type/ObjRefID/MinDif/stride/status) and the per-objective difficulty-gate
line -- both are the reusable signal for any future save/objective issue,
gated by g_savelogObjOnce + GE_SAVELOG so inert in normal play.
d43_emit.py reserved the PointUsage region (add_region) during layout but
the emit pass never wrote it -> all zeros -> chr.c's blood-decal walk
'index = PointUsage[index]' cycled 0->0 forever (capped by the #ifdef
PORT guard at chr.c:3328, so decals were missing/wrong, not a hang).
Now byteswaps each s16 index (numVertices entries, negative-terminated,
no remap). Full regen chain re-run, round-trip validation passes. The
chr.c guard stays as a safety net (SMALL-FIXES B3). Also: retract RC4
(palette off-by-one) in TEXTURE-GLITCH-ANALYSIS -- the current RGBA5551
decode is correct; the doc's 'spec' was ARGB1555.
ROOT CAUSE of 'complete Dam, Facility stays locked'. The type-23
objective record's MinDificulty is a big-endian s32 at word 3; on N64 the
value is its low byte == offset 0xF, and struct objective_entry reads
'difficulty' as s8 @0xF. d88_propdefs.py bswap32's word 3, so on
little-endian the value moves to offset 0xC -> objective_entry.difficulty
@0xF reads 0 for EVERY objective. get_difficulty_for_objective() then
returns Agent(0) for all, so objectiveIsAllComplete() on Agent evaluates
objectives that should be difficulty-gated out (Dam obj 0 'Neutralize all
alarms' is Secret Agent; obj 1/2 are 00 Agent) -> they're incomplete ->
never returns TRUE -> end_of_mission_briefing() / the unlock write never
runs. User was right: you don't touch an alarm on Agent Dam.
Fix (D151-class, #ifdef PORT): reorder struct objective_entry's word-3
tail so 'difficulty' reads @0xC (post-swap position); N64 layout kept
under #else. Also fixes dump_objectives.py (was reading byte 0xC = the
BE high byte = always 0; hence LEVEL-OBJECTIVES.md said 'Agent' for
everything). Dam now decodes obj0=Secret Agent, obj1/2=00 Agent,
obj3=Agent. -level_09 coverage identical (phash delta is the M-30 RC2
texture change, not this).
Run 2 crit dump: Dam obj 0 walk is correctly synced (type 23 START stride
4, then 4x type 25 DESTROY_OBJECT tags 0-3, stride 2) and ObjRefID reads
right (0,1,2,3). All 4 DESTROY criteria evaluate INCOMPLETE -> obj 0 not
complete -> objectiveIsAllComplete=0. Dam Agent obj A = 'Neutralize all
alarms' (4 ALARM-tagged boxes). Need to know if the alarms are (a) not
found, (b) found still-healthy because the player didn't shoot them, or
(c) found still-healthy after being shot = destruction-detection bug.
Added obj=/prop=/healthy= to the per-alarm SAVELOG line.
While the objective-completion -> save-unlock chain is being fixed (M-30
blocker), GE_UNLOCK_ALL=1 flips get_debug_enable_all_levels_flag() so the
front-end mission-select offers all 20 solo levels instead of just the
next unlocked one. #ifdef PORT, env-gated, one-shot cached; retail
behaviour unchanged without the var. Verified: mission-select renders all
20 level tiles, no crash. -level_09 unregressed.
First GE_SAVELOG run showed bossReturnTitleStage stage=33 (Dam) with
objectiveIsAllComplete=0 -> end_of_mission_briefing never called -> no
unlock. The loop bails at obj 0 (get_status_of_objective != COMPLETE).
Need to see what the criteria walk reads. Added a crit-level dump (type,
ObjRefID, TextID, sizepropdef stride, first 3 words, currentstatus),
gated behind g_savelogObjOnce which bossReturnTitleStage sets only around
its own objectiveIsAllComplete() call so the per-frame poll doesn't
flood. -level_09 renders clean, 0 SAVELOG lines without the env var.
User: complete Dam -> 'previous' from debrief -> mission select, Facility
still locked. data/ge007.eep has no completion times -> the unlock save
isn't landing. Chain is bossReturnTitleStage (objectiveIsAllComplete?) ->
end_of_mission_briefing (guard on briefingpage/difficulty) ->
fileUnlockStageInFolderAtDifficulty -> fileOverwriteSaveSlotWithNewSave
(needs a DORESET spare slot) -> fileWriteSave (fileGamePakProbe) ->
EEPROM, then fileValidateSaves CRC-checks on reload.
GE_SAVELOG=1 (env, #ifdef PORT, no logic change) logs each link:
objectiveIsAllComplete + per-objective diff/status, the briefing guard,
the unlock args + free-slot lookup, the write (probe/block/crc), and the
per-slot validate CRC pass/fail. -level_09 unregressed (diag is
getenv-gated; the framediff move is the earlier RC2 mip fix).
Reported: menu / room textures look 'interlaced' (alternating garbage
rows). Root cause (docs/TEXTURE-GLITCH-ANALYSIS.md RC2): GE's
texGetDepthAndSize() packs base level + every LOD mip into one
gDPLoadBlock, so fast3d's upload height (size_bytes / row) runs ~1.3x
taller than the base image and the mip bytes render as rows below it.
GE_DTEX=1 confirmed it: 64x64 I4 uploaded 64x87, 65x65 -> 65x95, etc.
Fix in import_texture(): when LOD is active (rdp.tex_lod) and the load
is a plain full-width block (not a windowed gfx_dp_load_tile - the D74
sub-tile / Rare-logo case, which is lod=0), clip size_bytes to the
SETTILESIZE base-tile height. GL then builds correct mips from a correct
base. New knob Video.FixMipTextures (default 1; =0 restores old upload,
verified byte-identical to golden -level_09 3/3). BUNKER1 with the fix
on: side-wall textures visibly cleaner, geometry/HUD unchanged; Silo
unregressed. -level_09/-level_20 framediff WILL flag with the fix on
(textures changed for the better) - refresh golden after the user
confirms the menu + Depot look right.
New GE_DTEX=1 env probe logs load params for the first 64 textures.
Reported: on SELECT FILE the mouse only moved side-to-side; up/down
needed W/S. The front-end cursor is stick-driven (frontUpdateControl
StickPosition reads joyGetStickX/Y); the port only fed mouse-X to the
stick in menus (mouse-Y went to C-buttons, which the menus ignore).
New: when current_menu != MENU_RUN_STAGE (and != MENU_INVALID, so bare
-level_XX in-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 global current_menu directly for UI context only - no logic change.
New knob Input.MenuPointerSpeed (%, default 100). -level_09 3/3
framediff; FILE_SELECT boots clean 900 frames.
The D63 branch-trail dump fired ~10 sysLogPrintf lines + a full trail
walk on EVERY unknown GBI opcode, unconditionally. On the front-end
menu (D149 corrupt model DL) this ran per-frame and collapsed the menu
to ~5fps with input starved. Removed the trail statics, the per-G_DL
recording, and the default-case dump; the rate-limited D146 'ending DL'
log stays. -level_09 3/3 framediff, -level_20 91.6% unregressed.