sync_framerate_with_timer() runs inline on the scheduler thread
(osSpTaskStartGo -> gfx_run -> swap_buffers_begin), so a low frame cap
blocks VI-retrace delivery and drags every game thread down to the cap
rate rather than just dropping presented frames. There is no N64 analogue
(RSP/RDP/VI are separate silicon; the CPU never waits on them).
Clamp 0 < FpsCap < 30 to 0 (uncapped) with a warning, at both:
- videoInit, so a bad ge007.ini value (e.g. dinged to 10 via the F10
options overlay) is normalised and persists sane on next configSave();
- gfx_sdl_set_target_fps, the fast3d chokepoint, which also covers the
overlay's live set path.
Caps >= 30 are unchanged. Default (FpsCap = 0) is byte-identical to
golden (-level_09 framediff 3/3). The real fix — moving frame pacing off
the scheduler thread — is documented as owed in findings.md D186.
New port/src/optionsoverlay.c + port/include/optionsoverlay.h: an
immediate-mode options overlay drawn as its own fast3d 2D display list,
appended after the game DL. Implements docs/dev/OPTIONS-MENU-PLAN.md §2/§3.
- config.c/h: configForEachOption() + configSetOptionMeta() label/step/enum
side table; CONFIG_OPT_* enum. config.c stays free of specific-key knowledge.
- gfx_pc.cpp gfx_run(): one-line hook runs optionsOverlayEmit() after the game
DL, before gfx_flush. Returns NULL when closed -> zero bytes appended, golden
dumps byte-identical.
- video.c: F10 toggles (videoPumpEvents, next to F12); ESC closes; wheel
scrolls; videoRequestLiveConfig()/liveCfgDirty re-applies VSync/FpsCap/
TextureFilter at frame start.
- input.c: while open, controller 0 returns a neutral pad and nav
(arrows/enter/wheel/mouse) routes to optionsOverlayHandleInput() — mirrors
the D180 WI-1 swallow pattern.
- v1 rows: VSync, FpsCap, MSAA(restart), TextureFilter, MouseAimSpeed,
MouseTurnSpeed, MouseInvertY, MouseCaptureMode, ScreenShakeIntensity.
Live-apply where possible; configSave() on close.
- GE_OPTIONSOVERLAY=1 auto-opens at boot (env-gated diagnostic, left in).
Port-layer only, no src/ edits, no #ifdef PORT in game code. Build links
241/241. Runtime framediff/soak/eyeball owed on an interactive machine
(this headless env kills the GUI process early and writes 0-byte PPMs).
MoveBond gates the entire ladder-collision path on
stanGetLocusCount(&curLocus), which was pinned at 0 on PC. Three compounding
pointer-width bugs in the stan navigation layer (same class as D79/D90),
no game logic touched:
1. stanCheckLinkedSpecialTile receives the caller's
struct StandTileLocusCallbackRecord typed as `s32 *outFlags` and writes
the LADDER signal with raw `outFlags[1] = 1`. The record's first member
is `s32 *rooms` — 8 bytes on PC — so `[1]` is its high half and `count`
(moved +4 -> +8) is never written. stanGetLocusCount() reads ->count,
stays 0, ladder path never runs. (FORCECROUCH survived by luck: LE low
half of rooms is at +0.) Fix: #ifdef PORT cast to the real struct, write
->rooms / ->count by name.
2. curLocus was declared `struct move_bond_temp_struct` — an 8-byte
"placeholder while matching" that equals the N64 record size but is too
small for the widened PC record, so stan's field writes overflowed the
local. Fix: #ifdef PORT declare it as the real
struct StandTileLocusCallbackRecord.
3. stanGetMoveBondCollisionTiles would AV on the first climb:
`(coord3d *)((s32)coords + off)` truncates the 64-bit &bondCollision
stack pointer. Fix: PORT_PTRADD (uintptr_t arithmetic) added to stan.c.
All #ifdef PORT; N64 path kept verbatim under #else. Candidate root-cause
from an M-36 subagent (parts 1/3); part 2 found on review.
Verified: build links clean; -level_09 framediff 3/3 (frame_200 run
variance only); -level_36 (Surface, mandatory ladders) boots crash-free;
interactive climb confirmed working in-game by the owner.
findings.md D177 (§F + §H), porting-notes.md §A.
Family A of the M-36 triage (Surface "grey static" D176b, file-select
interlacing D182-2). The assigned hypothesis — import_texture_* shear when
line_size_bytes != full_image_line_size_bytes — is DISPROVEN for the Surface
repro:
- Only gfx_dp_load_tile produces a strided load; GE loads every texture via
gDPLoadBlock (tex.c texWriteLoadToTmem*), which sets line == full. Measured:
0/64 GE_DTEX + 0/166 GE_TEXDUMP imports strided on -level_36; 0 in the
front end either.
- Independent offline pitch scan (new GE_TEXRAW dump) shows the importers
pick exactly the pitch the source is stored at. The Surface cliff texture
is a 32x32 IA8 whose ROM bytes are genuinely a noise field — decoded
faithfully, tiled ~1 texel/px. D176(b) is likely a tiling-density / UV /
shift-LOD issue (RC3/D167 family) or a wrong-texture bind, not a decode
bug. Full evidence: findings.md D183. D176(b)/D182(2) stay OPEN.
- Re-confirmed D159's texSwapAltRowBytes no-op is correct (re-applying it
makes the dumps worse).
Shipped anyway (golden-safe, reversible):
- import_texture() compacts strided rows into a contiguous buffer before the
format dispatch, so every importer sees line == full — the behaviour the
seven SUPPORT_CHECK(full == line) asserts only assert (and assert compiles
out under NDEBUG). Provably inert when full == line (all current loads).
GE_TEXPITCH=0 restores the old flat read.
- GE_DTEX gains line=/full=/STRIDED markers; GE_TEXRAW=1 dumps raw
importer-input bytes for offline decode-vs-source analysis.
Verified: build 240/240; -level_09 framediff 3/3; GE_STARTMENU=10 briefing
crash-free, renders. Investigation: subagent (worktree).
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
Family B of the M-36 triage. The Ubrief*Z segment (assets/obseg/brief/) is
a raw 48-byte ROM image of struct BriefStruct { u16 brief[4]; struct { u16
textid; u16 enabled_difficulty; } objective[10]; }, loaded by front.c
load_briefing_text_for_stage() via _fileNameLoadToAddr() with no converter
and no BE->LE fixup. On the LE host all 24 u16 read byte-swapped:
- textid 0x2C04 (getStringID(LDAM,4)) -> 0x042C -> langGet() resolves an
unloaded bank -> NULL -> blank objective/briefing text. This is the whole
of the D143 "briefing text blank" residual (the bank loaded fine; the ids
into it were swapped).
- enabled_difficulty 0x0001 -> 0x0100 = 256, so `selected_difficulty >=
enabled_difficulty` is always false -> difficulty-gated objectives vanish
(on Agent only the one AGENT-difficulty line survived -> lone "a." bullet).
Fix: romdataFixupBriefing() in port/src/romdata.c, called from a #ifdef PORT
block in load_briefing_text_for_stage() right after the load — same shape as
langFixupLoadedBank(). BE-serialized-struct reconciliation (AGENTS.md #2),
semantics-preserving, N64 build untouched. GE_D178=1 dumps raw + fixed words.
Verified: Dam briefing renders all four objectives at 00 Agent with correct
difficulty gating and "007: James Bond" header; -level_09 framediff 3/3;
GE_STARTMENU=7/=13 crash-free; build links clean 240/240.
Investigation + patch: subagent (worktree). Suspect 2 (Gfx-width temp_s0
arithmetic) was ruled out — benign 512-byte gap, langLoadToBank records the
bank base itself. Follow-ups noted in findings.md: annotate D143 as
superseded; latent 64->32 pointer truncation at front.c:6569 / :2202.
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
- README: attract-mode stills + demo GIF as the hero row, status/warning
callouts, metrics table, "Contents" index, doc-index table; simplified
the two-agent workflow diagram (LR -> TD, fewer edges) for GitHub web
- Table-of-contents indexes added to internals.md, porting-notes.md,
agentic-development.md, AUDIO-PLAN.md
- CI: add Windows/MSYS2 build job, minimal token (contents: read),
persist-credentials: false, concurrency group; run on main + master.
validate job regenerates the US ROM-asset symbol file only and configures
ntsc-final (PAL/JP need a region ROM; no ROM is used in CI)
- .github: SECURITY.md, issue forms + chooser config, PR template,
labels.yml, dependabot (Actions, monthly)
- Do not track the proprietary SGI IRIX IDO toolchain or SGIImageViewer.exe;
document in SetupGuide.md / building.md how to obtain IDO for N64 ROM
builds (the PC port does not need it)
- Stop tracking two committed *.pyc files; drop stale screenshots and a
redundant .gitignore line; misc doc cross-reference fixes
Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
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, 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.