forked from Ports/goldeneye-pc-port
* D250: fix sysSleep unit-conversion bug in frame-pacing; add GE_D250 lvlRender probe port/fast3d/gfx_sdl2.cpp sync_framerate_with_timer() computed the remaining frame budget in 100ns units (qpc_to_100ns) and passed it straight into sysSleep(), which takes microseconds -- a real 10x oversleep whenever that branch fires. Genuine bug, but only active when target_fps is nonzero (tied to Input/Video FpsCap); confirmed via live testing that it is NOT the active cause of the user's reported ~45-50fps with FpsCap=0/VSync=0, where this pacing path never runs. Added GE_D250 (src/boss.c, #ifdef PORT) to isolate lvlRender()'s DL-building cost (culling/animation/scene traversal) from gfx_run's already-measured software-RSP interpretation cost. Live test on Dam shows lvlRender averaging ~0.5-1.5ms -- also not the bottleneck. Documented in docs/dev/GE-ENV-PROBES.md (GE_D250 row). findings.md D250 update to follow after rebase. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs(D250): live A/B session -- MSAA/aniso/FOV/resolution/vsync all ruled out Live session with the user driving the game directly, confirmed the sim-healthy/render-sagging pattern from earlier idle testing reproduces under real play on Dam. One-at-a-time A/B via the F10 overlay ruled out MSAA, anisotropy, FOV, resolution (1280x960 fullscreen vs 640x480 windowed), and VSync (render never exceeds ~60 even uncapped) as the cause -- the cost is resolution/GPU-setting-independent. gfx_run (~6-10ms) + lvlRender (~0.5-1.5ms, via the new GE_D250 probe) together don't account for the real ~20-22ms/frame. Leading remaining suspect: the emulated N64 thread-handoff/scheduler round-trip (gesched.c/sched.c) -- real OS context-switch/wake latency standing in for what was free hardware handoff on N64. Needs a real profiler (Visual Studio / WPR) attached during a live bad-fps moment to confirm; ad-hoc per-call timers have now ruled out the two most obvious CPU call sites without finding the cost. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * D250: fix uncached getenv() in fast3d hot paths -- root cause of the fps drop A live WPR/xperf CPU trace (tools_pc/perf.ps1, new sister script to debug.ps1) with Microsoft public symbols resolved found the dominant render thread spending only 5.2% of its CPU time in the game's own code -- 43% was in msvcrt.dll, and getenv()/_getenv_helper_nolock alone accounted for ~50% of the thread's total CPU time. Root cause: port/fast3d/gfx_pc.cpp's import_texture() (runs on every texture bind) and several per-triangle/per-G_SETCOMBINE debug-probe checks (GE_DTEX, GE_TEXDUMP, GE_TEXRAW, GE_D229 x2, GE_D172 x2, plus GE_TEXDUMP/GE_PCDUMP in gfx_opengl.cpp) called getenv() uncached on every call, unlike the safe static-cached pattern already used elsewhere in this codebase. On a texture-heavy scene that's tens of thousands of locked, locale-aware Windows CRT environment lookups per second -- invisible to both of the timers added earlier this session (gfx_run's own cost measurement and the new GE_D250 lvlRender probe), since neither isolates CRT/library time from the game's own code. Fix: cache every getenv() site in gfx_pc.cpp/gfx_opengl.cpp with the established static-int pattern. Narrow, mechanical, no logic change. Verified: the exact idle-Dam test that was pinned at 45-50fps before the fix now holds a rock-steady 60-61fps, zero dips. Also explains why the earlier MSAA/aniso/FOV/resolution/vsync A/B pass all showed "no change" -- the getenv() cost was a large per-call constant that swamped any real, smaller resolution-dependent GPU cost at every setting tried. Residual, separate issue found once this was fixed: still runs badly at 1440p+ (640x480 now holds a clean 60fps) -- likely the real GPU fill-rate cost the getenv() bug was masking, now exposed on its own. Not yet investigated. docs/dev/findings.md D250 updated with the full root-cause writeup. docs/porting-notes.md gets a new lesson under section E (cache getenv() in any hot-path probe). tools_pc/perf.ps1 automates the trace-and-analyze workflow for next time. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs(D250): correct the 1440p+ follow-up -- it was a stale-build false alarm User re-tested 1440p/4K on the actual getenv-fix build and confirmed it holds up fine too. The "still runs badly at 1440p+" report was from before the fix was live, same stale-build class as the DLL-load false alarm earlier in the session. D250 is fully resolved across all resolutions tested, not just 640x480. Claude-Session: https://claude.ai/code/session_016DQUKGLZxFtPy52MkKXXog * docs: add missing GE_D219RAW row to GE-ENV-PROBES.md (CI validate fix) Pre-existing gap unrelated to this branch's own changes -- gen_env_probes.py --check caught it now since it runs on every PR. GE_D219RAW has had live getenv() sites in src/game/image.c since the D219 investigation but was never added to the probe doc.
tools_pc/ — PC-port dev & test scripts
Index for the ~60 scripts here (speed-ups plan Step 4 / N2). None of these are part of the build; they are converters, verification tooling, and investigation artifacts.
Marker:
living— kept and re-run; part of the verification ritual, the release flow, or a converter the build depends on (via the sidecars it emits).artifact— added to chase one finding. Safe to move totools_pc/archive/<dxx>/once that finding is closed and the technique is captured indocs/dev/findings.md/docs/porting-notes.md.
Living — converters the sidecars depend on
| Script | Role |
|---|---|
d43_emit.py |
Offline N64→PC model-file converter → RZ sidecar + manifest.csv (D50 / Plan B). Regen after any model-format change. |
d43_convert.py |
Reference single-file model converter + full-512 layout/pointer validator (D43). |
d69_emit.py |
Offline converter for stage bg/*.seg + Tbg_*_stanZ → concatenated sidecar (D69/D78–D82). |
d88_emit.py |
Offline converter for per-level Usetup*Z stage-setup files → appended to the pccg.bin sidecar (D88). |
d88_propdefs.py |
The propDefs polymorphic-record stream N64→PC converter used by d88_emit.py (D88.4). |
d69_emit.py / d88_emit.py / d43_emit.py |
Run all three (+ d88_propdefs) to rebuild data/ sidecars from the ROM. |
Living — verification & release
| Script | Role |
|---|---|
verify.sh |
Speed-ups plan Step 2. One command → one verdict: build-if-stale, headless GE_PCDUMP/GE_INPUTSCRIPT capture, crash-detect + symbolication (via crash_brief.py), pixcount, framediff vs golden, --json. verify.sh <level>, verify.sh sweep [subset], verify.sh parity <level> --against <dir>. |
crash_brief.py |
Speed-ups plan Step 9 / R5. ge007.crash.log → symbolicated frames + a dev-process.md-shaped dispatch-ready brief: matches porting-notes.md sections / findings-index.csv entries by resolved function/file, flags known parked-crash signatures (Cuba credits, D188 va_list, D189 stack overrun) so agents stop re-investigating them. Called automatically by verify.sh on CRASH; also runnable standalone. |
framediff.py |
Visual regression: candidate GE_PCDUMP frames vs tools_pc/golden/, per-region divergence. Structural/tolerant by default; --exact after GE_DETERM. |
pixcount.py |
Count non-black pixels in a PPM dump — "did the scene render anything" as a number. |
level_sweep.sh |
Bare -level_XX boot of all 21 solo levels → PASS / NO-FRAMES / CRASH. Predates verify.sh sweep; kept as the battle-tested full-21-level runner until verify.sh sweep has done an equivalent soak. |
playtest.sh |
Launch a level for docs/dev/LEVEL-PLAYTEST.md human validation (WS6). |
audiodebug.ps1 |
Audio counterpart to debug.ps1 (that one is for crashes; this one is for audio behaviour). Drives the GE_D204 health monitor + GE_AUDIOTRACE/GE_AUDIODUMP and prints a pass/fail verdict: real-time ratio, queue starvation, dropped blocks, oversized blocks, plus a soundIndex histogram. -AB runs the same binary twice (with and without GE_D204_OLD) and prints a before/after table — how D204 was measured. -Play for an instrumented interactive playtest (the D202 workflow), -Soak for a 5-min stability run, -SyncData to mirror ./data first. |
debug.ps1 / repro_gdb.sh / attach_animgen.sh |
Launch (or attach to) the game under gdb so a crash always leaves a backtrace. |
bundle-win.sh / bundle-linux.sh |
Package a built tree as a distributable archive (exe + licenses + prepare-assets/). |
romverify.c |
One-shot .z64 integrity check against the repo's ground truths. |
dump_objectives.py |
Dump per-level objectives + win/fail criteria from the ROM (playtest validation; Step 10 / N8 acceptance oracle). |
disasm.py |
Minimal MIPS disassembler for the BE ROM (RAM-addr → file offset). |
ppm2bmp.py |
PPM → 24-bit BMP, no deps — eyeball GE_PCDUMP frames without PIL. |
gen_findings_index.py |
Regenerate docs/dev/findings-index.csv (grep-before-you-read aid for the 200 KB finding log). --check in CI-style use. |
gen_env_probes.py |
Drift check for docs/dev/GE-ENV-PROBES.md — re-greps live getenv("GE_*") sites, reports NEW/GONE. |
Living — compiler-verified layout probes (kept: re-run when structs change)
| Script | Role |
|---|---|
d43_layoutprobe.c |
Prints sizeof/offsetof for every struct d43_emit.py must reproduce, built with the port toolchain. |
d88_layoutprobe.c |
Same, for the PROPDEF_* record structs d88_emit.py converts. |
mtxtest.c |
Standalone numerical test of the fast3d matrix pipeline conventions (GE swapped-perspective, packing, MUL order). |
Investigation artifacts — D43 model-file format (finding closed; archive candidates)
d43_chainbound.py, d43_cover.py, d43_decode.py, d43_fullwalk.py,
d43_gdldump.py, d43_gdlhist.py, d43_gdlorder.py, d43_gdlseq.py,
d43_invariants.py, d43_layout.py, d43_layout1.py, d43_lutscan.py,
d43_pointusage.py, d43_seg5.py, d43_seg5ops.py, d43_seg5vtx.py,
d43_seg5vtx2.py, d43_sizes.py, d43_sizes2.py, d43_tree_dump.py,
d43_vtxfmt.py, d43_walk.py — layout/pointer/GDL-order analysis passes that
derived the D43 converter spec. d43_sizes.py is still exec()'d by a couple
of the others.
Investigation artifacts — other closed/parked findings
| Script | Finding |
|---|---|
d125_check.py |
D125 — is the emitted propDefs blob byte-identical to d88_propdefs.convert_stream()? |
d88_propdef_scan.py |
D88.4 — histogram of PROPDEF_* record types across shipped levels. |
d51_gdb.py / dump_animgen.cmd |
D51 texture-tile crash capture / Facility-outro-hang anim-state dump (gdb scripts). |
Other
dist/— staged bundle README template (README.md.in) + tokens.golden/— per-level golden frames forframediff.py(Step 3 fills this out).__pycache__/— ignored.