Commit Graph
537 Commits
Author SHA1 Message Date
James Dansereau 8f615bde14 docs(D223): new — directional mouse-wheel weapon cycling, root-caused
QoL ask: wheel up = next weapon, wheel down = previous, instead of both
directions cycling forward. Root-caused: GE already computes separate
weaponForwardOffset/weaponBackOffset signals from
invButtons(A)+shootButtons(Z) held/edge combos (bondview2.c:5338-5347,
the N64 'hold A tap Z' backward-cycle trick); triggerOn is suppressed
while invButtons is held so this never accidentally fires. Port's
inputPostWheel() discards wheel direction and only ever synthesizes the
forward (bare A pulse) case. Fix is port-only (input.c), synthesize the
A-held+G-edge combo for wheel-down instead - no game-logic change.
Open question: which N64 controldef preset is actually active in port
saves, since invButtons/shootButtons differ per preset.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:18:48 -04:00
James Dansereau 9016cf183c docs(D222): new — high FovScale breaks culling, NPCs/objects vanish near screen edges
Split out of D211's 'cosmetic edge-cull-early' caveat - user confirms
this is a real functional bug, not cosmetic: NPCs and some world objects
stop rendering near screen edges at high FovScale. Root cause: frustum
cull / visibility math still runs at nominal unscaled fovy while the
render projection is portFovScale-widened. Fix direction + a real risk
flagged: some of these cull call sites may double as AI
awareness/visibility checks, not just render culling - must audit before
changing so portFovScale never leaks into NPC behavior (non-negotiable
#2, game logic unmodified).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:14:37 -04:00
James Dansereau 4e96a60b3f docs(D211/D218): M-87 QoL asks — FOV in real degrees, draw-distance/LOD wanted
D211/WIDESCREEN-FOV-PLAN: user wants Video.FovScale exposed as a real
horizontal-or-vertical degree value instead of the current
percent-of-original-vertical-FOV scale. Not critical, backlogged. Noted
the cleaner path (relabel as degrees, drop the percent indirection) vs
keeping percent internally and just displaying the computed degrees.

D218: user confirms the high-FOV draw-distance/fog artifact still
occurs, and now wants Video.DrawDistance implemented (previously
declined) plus broadened to draw-distance/LOD in general, not just the
fog-boundary case this row root-caused. Open question logged: does GE
have a separate object/geometry LOD-swap distance mechanism worth
exposing, or is Visibility.FarFog the whole story.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:13:07 -04:00
James Dansereau d3e3819561 docs(D194): M-87 design-goal note — mouse feel should target Quake/PD-remaster responsiveness without losing GE aim nuance
User wants overall mouse feel closer to Quake Remaster / the Perfect Dark
PC port (raw, direct, responsive) while explicitly preserving GE's
aim-mode vs hipfire split (the reason D118/D194 exist). Reframes the fix
order: normalize by a fixed reference dt first (D194 part b, the
frame-rate coupling - likely the dominant cause of 'feels
unresponsive'), then re-derive the aim-mode curve, and evaluate raw
PD/Quake-style hipfire mouse-look as a real option outside aim mode
rather than assuming N64 stick-emulation is mandatory everywhere.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:11:08 -04:00
James Dansereau 60a31cbc2e docs: M-87 playtest results — D178/D149 fixed, D182 still broken, D77 rescoped, D221 new
D178 (briefing objectives blank) and D149 (front-end 3D models garbled) -
user-verified fixed, closed.
D182 (file-select round-trip degradation) - still broken, confirmed.
D77 (no music) - rescoped: music plays on intro/cast-roll/main menu, only
missing in-level. Points at the level-start music cue, not the
mixer/playback layer.
D221 (new): SELECT FILE mouse hit-box vertically offset from the visible
box (click below a file to select it) under the shipped default
MouseCaptureMode=1 - distinct from D192's clamp-range bug under legacy
mode 0, likely same front-end grid-coordinate family.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:10:30 -04:00
James Dansereau c82430abcf docs: M-87 playtest notes — D176/D173/D148 still current
D176: skybox still glitchy broadly (not just Surface); Cradle named as
still showing solid black where sky/backdrop should be. Confirms (a)
never landed a verified fix.
D173: Bond hover/float cutscene glitch still present.
D148/D160: new specific instance - Bond spins in circles in some
cutscenes (end-of-Cradle Trevelyan-falls cutscene named) - likely same
actor-position/anim-root cause as D173, manifesting as bad rotation.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:07:11 -04:00
James Dansereau ad82c0cac6 docs: M-87 playtest notes — D211 good, D217 matrix, D219/D220 logged
D211: user-verified good at big FovScale.
D217: playtest matrix across weapons/levels (PPK/AK47/M16, Dam/Facility/
Jungle) - green, blue, white (missing), orange, all consistent with the
per-weapon-material upstream theory, not one fixed offset. Watch still
green.
D219 (new): PPK explosions render purple on Dam - likely D172 particle-
color family, different DL.
D220 (new): 1P muzzle flash shifts upward at higher FovScale (Jungle
M16) - likely same unscaled-viewmodel-fovy root as D211's residual
caveat.

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 22:04:13 -04:00
James Dansereau 6b66c9ddc9 docs(D217/D211): M-86 — D217 root cause characterised; D211 aim-zoom verified
D217: two defects found. (1) The visible green grip/hand is upstream of
fast3d — the grip's count=1 model G_LOADTLUT is fed different source bytes
per level (Facility 1a 00 -> 0x1A00 green; Bunker 00 01 -> black); every
larger model TLUT carries a 2-entry leading zero pad, so the port's
model-TLUT source pointer is likely 4 bytes low (or a d43_emit.py blob
offset). fast3d byteswap is correct (proven by Bunker). (2) A real CI
texture-cache collision (no palette-content key) — fix landed on branch
fix/d217-model-texture-palette f6fc0399 (palette FNV-1a hash in the CI
key); owes a 21-level sweep; does not fix (1). porting-notes + graphics-
backlog updated.

D211: aim-zoom compose verified correct by construction (single post-lerp
multiply at the fr.c chokepoint; zoom ratio preserved). Only a low-risk
cosmetic eyeball remains (viewmodel/frustum-cull use unscaled fovy).

Claude-Session: https://claude.ai/code/session_016TaKy6AAsVUTfjkhrX6CjG
2026-09-10 18:40:48 -04:00
James Dansereau 2cdecd3aa6 docs(D217): rule out stale-tail; best theory = CI-cache/palette collision
GE_D217Z experiment (zero rdp.palette[count..255] after each partial
load) did NOT change the green grip -> bad texels index within [0..count),
so the wrong palette is *bound* to the grip tile, not tail overflow.
Best remaining theory: TextureCacheKey collision / missing CI-cache
invalidation on palette change (key has no palette-content hash). Facility
renders green-uniformed guards right before the viewmodel -> a stale
cache HIT / palette_addrs collision makes the grip decode against the
guard-fatigue palette. Next probes + the all-levels regression
requirement noted in the finding.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:43:38 -04:00
James Dansereau 2e3d8be7e6 docs(D217): M-85 probe results — partial-count TLUT + stale upper palette
User confirms D217 is widespread (many levels, multiple weapons), not
Facility-specific. GE_D217 temp probe (reverted) shows GE weapon/character
model DLs issue partial-count G_LOADTLUT commands (count 1/28/125/172/193/
...); count=125 writes rdp.palette[0..124] and leaves [125..255] holding
the previous TLUT's entries, which differ per level. Weapon TLUT sequence
is identical BUNKER1 vs FACILITY (same PP7) + tiles import palidx=0
cache=HIT, so the delta is purely which stale upper-palette entry a weapon
CI8 texel lands in when it indexes >= its palette count. Root cause open:
CI8 decode reading past the base image (RC2/D183) vs GE relying on N64
TMEM-TLUT leftover semantics. Concrete next probes + a zero-the-tail
experiment noted in the finding. Wants a dedicated debug session.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:40:50 -04:00
James Dansereau 717a5210d3 docs(D217): repro pinned to FACILITY post-cutscene
BUNKER1 is clean in any config; -level_34 FACILITY reproduces at ~frame
300 (just after the vent-drop cutscene). PP7 grip = solid green block,
green ring at the barrel joint, hand has a green cast; slide/barrel upper
is correct black. GE_TEXI confirms the weapon CI tiles import with a
loaded TLUT (fmt=2 palfmt=0x8000 palidx=0) -> gDPLoadTLUT does reach
gfx_pc.cpp for model DLs. Green-tinted hand => wrong TLUT bound, not
corrupt index data. Working theory: FACILITY pre-level cutscene leaves
fast3d palette/tile state (or a TextureCacheKey collision on
rdp.palette_addrs[] at gfx_pc.cpp:990) that the cutsceneless BUNKER1
never hits. Next steps + suggested subagent scope in the finding.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:33:34 -04:00
James Dansereau f9ff24101a docs(D217/D211): M-85 triage results
D217: did not reproduce headlessly on main @ 3fa1ba53. GE_DTEX/GE_TEXI/
GE_TEXDUMP on -level_09 across FovScale 100/120/150, TextureFilter 1&2,
FixMipTextures 1&0, Anisotropy 4&16 -> PP7 + hand + guard models all
render clean. gDPLoadTLUT DOES reach gfx_pc.cpp for model DLs (weapon CI
tiles import palfmt=0x8000 with non-stale palettes) -> the stale-rdp.palette
hypothesis is disproven for the BUNKER1 start pose. Row updated with the
remaining leads; blocked on a user screenshot of the actual defect.

D211: headless FovScale 100-vs-120 diff confirms HUD-static (ammo counter
screen position identical, no 4:3 stretch, viewmodel composes). aim-zoom
eyeball still owed.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:22:44 -04:00
James Dansereau 40e875bcbb tools_pc/golden: re-baseline bunker1 frames for D215 viewmodel
D215 (#37, merged 73768a03) un-gated the 1P weapon-model DL nodes, so the
PP7 + hand now render in the -level_09 intro-pan capture window. The
committed golden predated that, making `verify.sh bunker1` report
REGRESSION (worst_cell ~28) on every run.

Captured on main @ 3fa1ba53, clean default ini pinned to 640x480,
GE_PCDUMP=200-440:120. Gun eyeball-confirmed visible + correctly posed +
clean (no D217 garble in headless capture). Post-rebaseline
`verify.sh bunker1` = PASS worst_cell 12.3.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-09 23:20:05 -04:00
James Dansereau 3fa1ba534d Merge pull request #38 from jkdansereau/feat/d216-skip-intro
feat(D216): Game.SkipIntro — boot straight to the file-select menu
2026-09-09 20:57:09 -04:00
James Dansereau 208fcfe27d Merge main into feat/d216-skip-intro (M-84 + D215 findings rows)
# Conflicts:
#	docs/dev/QOL-INVENTORY.md
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
#	port/src/video.c
2026-09-09 20:54:05 -04:00
James Dansereau 73768a0357 Merge pull request #37 from jkdansereau/fix/d215-viewmodel-renderdata-flags
fix(D215): 1P weapon viewmodel never drew in-level — renderdata.flags read 0 on x86-64
2026-09-09 20:52:40 -04:00
James Dansereau 9c623858c1 regen findings-index (validate gate); M-84 verdict tokens on D211/D214/D217 status cells 2026-09-09 20:49:57 -04:00
James Dansereau 6e5a4e4e7f Merge main into fix/d215-viewmodel-renderdata-flags (M-84 findings rows)
# Conflicts:
#	docs/dev/findings-index.csv
#	docs/dev/findings.md
2026-09-09 20:37:22 -04:00
James Dansereau 9a667bd0b0 Merge pull request #36 from jkdansereau/feat/pc-qol-fov-aniso-fps
feat(D211-D214): PC QoL wave — FOV slider, anisotropic, on-screen FPS, keyboard rebinding
2026-09-09 20:35:03 -04:00
James Dansereau b3bf7786ff M-84: fold D211 FOV refix + user-test findings into the QoL wave
D211 didn't affect the in-level view (user test): the fast3d
gfx_apply_fov_scale hook only recognised *pure* perspective matrix loads,
but GE draws the world through g_CurrentPlayer->field_10E0 -- a
projection x view matrix pre-combined on the CPU (bondview2.c:8224) --
which failed the guard. Only the pause/watch model + sky FOV changed.

Fix: scale fovy game-side at the guPerspectiveF chokepoint
(src/fr.c, #ifdef PORT, frFovY *= portFovScale, clamp 160deg), which
lands before the CPU combine. Removed the fast3d hook + gfx_api.h decl;
port/src/video.c now sets f32 portFovScale. Gated to
lvlGetCurrentStageToLoad() != LEVELID_TITLE so the front end (which
renders through the same path) keeps its authored FOV.

User-verified: menu unaffected, in-level view widens (Dam tunnel).
Caveat: currentPlayerSetCameraScale keeps nominal FOV for frustum-cull
/ screen<->world math (edge geometry culls slightly early at high scale).

Adds src/fr.c as a #ifdef PORT game-code touch (D181 policy class).

Docs (this test pass):
- D214: config.c sections by the last dot, so binds live in [Input.Bind]
  not [Bind]. Doc-only fix (finding, index, QOL-INVENTORY); code unchanged.
- D217 (OPEN): viewmodel + 3rd-person Bond model textures render garbled
  -- stale-palette CI-texture bug (D161 family), surfaced by D215.
- D218 (LOGGED, parked): wide FOV exposes the per-level FarFog
  draw-distance boundary; Video.DrawDistance fix proposed.
2026-09-09 20:30:21 -04:00
James Dansereau da25c4c507 feat(D216): Game.SkipIntro — boot straight to the file-select menu
New s32 portSkipIntro (port/src/video.c, configRegisterInt Game.SkipIntro),
read once in src/game/lv.c at LEVELID_TITLE stage load, in the else branch of
the existing GE_STARTMENU #ifdef PORT block. When set it performs the same
state change every intro stage's own !is_first_time_on_main_menu branch does
(front.c): is_first_time_on_main_menu=FALSE; prev_keypresses=TRUE;
maybe_is_in_menu=TRUE; menu_update=MENU_FILE_SELECT. So it reuses the game's
own post-intro route rather than inventing one; the legal screen + logo
attract loop is skipped. GE_STARTMENU still wins when both are set.

Default 0 => unchanged. Verified headless: default boot frame 90 = the
TWYCROSS classification screen; SkipIntro=1 frame 90 = the SELECT FILE menu
(folders/photos/cursor render, no crash across repeated boots). Screenshots
eyeballed.

findings.md D216 + index; QOL-INVENTORY row marked done.
2026-09-09 01:07:05 -04:00
James Dansereau 152b70a760 docs(D215): add GE_DVM to GE-ENV-PROBES.md (validate gate) 2026-09-09 00:58:29 -04:00
James Dansereau f602b1738f fix(D215): 1P weapon viewmodel never drew in-level — renderdata.flags read 0
gunRenderFirstPersonGunModels built its ModelRenderData with
  renderdata = *(ModelRenderData *)&D_80035CC0;
a reinterpret that reads the lone `u32 D_80035CC0 = 0` and continues into the
next declared global `u32 D_80035CC4[] = {1, 3, 0, ...}` (gun.c). On N64 that
yields { basemtx=0, zbufferenabled=1, flags=3, rest=0 } because the linker
lays the two globals out contiguously in declaration order and pointers are
4 bytes. On x86-64 it breaks twice: ModelRenderData's pointer members widen
4->8 B (so `flags` is no longer at struct offset 8) and C guarantees nothing
about the two globals' placement. Measured (GE_DVM=1, BUNKER1): flags == 0,
which gates every weapon-model DL node off in modelRenderNodeGundl
((renderdata->flags & 1)) -> subdraw emits 1 gfx command instead of ~150 ->
no gun visible while playing.

All three VIEWMODEL-RESEARCH.md ranked suspects were ruled out first by the
same probe: field_87F==1, RootNode valid, numRecords 22 < pool 192, node walk
visits 24+ valid DL/SWITCH/BSP nodes.

Fix: #ifdef PORT -> set the fields explicitly (zbufferenabled=TRUE; flags=3);
N64 line verbatim under #else. ABI/layout only (D3x class), no logic change.

Verified (headless): GE_DVM=1 post-fix shows flags=0x3 and the weapon subdraw
now emits 153 gfx commands (was 1); bunker1 framediff worst_cell 12.2 -> 28.5
(gun renders in-frame); Dam/Facility/Bunker1/Archives/Cradle/Aztec sweep
crash-free. Owed: one BUNKER1 eyeball; the bunker1 golden needs re-baselining.

GE_DVM gate+render probes left in, env-gated (cf. GE_D193*).
findings.md D215 + index; GRAPHICS-BACKLOG + VIEWMODEL-RESEARCH marked resolved.
2026-09-09 00:56:19 -04:00
James Dansereau de31f2874b feat(D214): keyboard rebinding via [Bind] config section
port/src/input.c: 12 gameplay actions (Forward/Back/StrafeLeft/StrafeRight/
TurnLeft/TurnRight/Fire/Aim/Action/Cancel/LeanLeft/Start), each bound to a
comma-separated list of up to 4 SDL scancode names via Input.Bind.* keys.
inputRebuildBinds() parses them in inputInit() after configLoad();
SDL_GetScancodeFromName resolves names, unknowns warn + skip. The hardcoded
keyDown(ks, SDL_SCANCODE_*) block in inputComputePad is replaced with
actHeld(ks, IA_*). Mouse buttons (fire/aim) stay hardwired.

File-driven, no UI -- matches the PD port. A fresh ge007.ini gets the full
[Bind] block with defaults visible.

Defaults reproduce the previous FPS layout exactly: all 20 default key names
round-trip to the identical SDL_Scancode the old code used (0 mismatches in a
SDL_GetScancodeFromName test). Build clean; verify.sh dam/facility/bunker1
PASS crash-free.

findings.md D214 + index; QOL-INVENTORY row marked done.
2026-09-09 00:09:26 -04:00
James Dansereau 7bcfbf313c feat(D211-D213): PC QoL image options — FOV slider, anisotropic filtering, on-screen FPS
Three port-only knobs, each defaulting to byte-identical behaviour:

- Video.FovScale (D211): multiplicative half-angle scale on every perspective
  projection matrix as it is loaded (gfx_apply_fov_scale, gfx_pc.cpp). Columns
  0+1 scaled together so H/V FOV widen uniformly and aspect is preserved; ortho
  (HUD/menu) matrices untouched. Composes with the game's per-frame aim-zoom
  rewrites for free. Percent of vertical FOV, 50-150, default 100 => guarded
  early-return (zero FP ops, golden dumps byte-identical). WIDESCREEN-FOV-PLAN
  Phase 4, landed standalone.

- Video.Anisotropy (D212): expose fast3d's already-implemented GL anisotropy
  hook as config (1-16). New gfx_set_anisotropy_level() extern-C wrapper clamps
  to the GL max. Default 4 = the value fast3d already hardcoded => no delta.

- Video.DisplayFPS (D213): PD-parity top-right FPS readout, sampled once per
  emitted frame (optionsoverlay.c fpsTick). Config-only; default 0 => emit
  still returns NULL with the panel closed.

F10 overlay gains 'Anisotropic' and 'FOV scale %' rows (DisplayFPS is file-only
- the panel is at its 13-row layout limit, matching PD).

Build clean (ntsc-final); bunker1 framediff worst_cell 12.219 vs 12.221
baseline = render noise, no-op confirmed. Interactive eyeballs owed (see
docs/HANDOFF.md M-83).

findings.md: D211-D213 rows + index regen. WIDESCREEN-FOV-PLAN Phase 4 and
QOL-INVENTORY rows marked done.
2026-09-09 00:02:47 -04:00
James Dansereau 8742b93f29 Merge pull request #34 from jkdansereau/docs/d172-0xb9-withdrawn
fix(D172): particle magenta/cyan — gfx_lod_tile_offset collapses non-LOD 2-texture combines
2026-09-08 23:40:52 -04:00
James Dansereau fb8557b2da docs(D172): add GE_D172 to GE-ENV-PROBES.md (validate gate) 2026-09-08 23:37:49 -04:00
James Dansereau 9b649b025d docs(D172): ROOT CAUSE FOUND + FIXED — gfx_lod_tile_offset collapse; findings D172 row + index 2026-09-08 23:31:47 -04:00
James Dansereau 157a8de59a fix(D172): stop gfx_lod_tile_offset collapsing non-LOD 2-texture combines
ROOT CAUSE FOUND (GE_D172 probe, user Silo firefight capture): particle
draws (G_CC_INTERFERENCE = TEXEL0*TEXEL1) configure tile 0 = IA8 smoke @
tmem 0 and tile 1 = RGBA16 fire @ tmem 0x188, but gfx_lod_tile_offset()
returns 0 unconditionally on the !gfx_detail_textures_enabled path (a D107
blurry-ceiling fix), so texunit 1 samples the smoke texture too. smoke *
smoke -> the magenta/cyan blood/spark colour.

Probe evidence, 40/40 particle tris:
  cfg1[tile=1 tmem=392 fmt=RGBA siz=16b]  SAMPLED1=tile0(tmem=0 fmt=IA)

Fix: only fold texunit i to the base tile when LOD is actually active
(rdp.tex_lod). D107's mip case (tex_lod=1) is unchanged; genuine non-LOD
multitexture now gets tile i. bunker1 verify PASS (worst_cell 11.3, the
D107 repro level, no regression).

Needs an in-game eyeball: blood/sparks should read dark red, not magenta.
2026-09-08 23:30:45 -04:00
James Dansereau 9ad69679b4 diag(D172): probe only non-LOD multitex (tex_lod=0) — the particle case 2026-09-08 23:25:26 -04:00
James Dansereau e65830d980 diag(D172): sharper probe — filter to real multitex, log tex_lod + sampled vs configured tile
The 16-line budget filled during the Silo intro with LOD-bilerp tris
(TEXEL1 = mip level, T0==T1 is correct there). New probe: only fires when
the DL configured tile first+1 at a DISTINCT tmem (genuine 2nd material),
logs tex_lod + which tile each texunit actually SAMPLES vs what was
configured, flags TEXEL1==TEXEL0. Budget 40.
2026-09-08 23:22:32 -04:00
James Dansereau 0c6173f1bd diag(D172): replace weak tri probe with a decisive TEXEL1-bind dump
The old 'cyc2 combine at cycletype=1CYC' probe over-fired on trivial
passthrough cyc2 terms. Replace with one that fires only when a 2-cycle
triangle actually consumes TEXEL1 (the G_CC_INTERFERENCE particle case) and
dumps both texunits' tile/tmem/fmt/siz. One Silo capture then shows whether
TEXEL1 resolves to the fire tile (tmem 392, RGBA16) or something wrong.

Build clean; bunker1 verify PASS.
2026-09-08 23:12:09 -04:00
James Dansereau 9215fde4fb diag(D172): rule out cycle-type too; ship GE_D172 probes
Real particle records (assets/oddtextures.c globalDL_0x078..) DO set
gsDPSetCycleType(G_CYC_2CYCLE); GE_D172=1 probe confirms fast3d applies it
(particle combine fc26a004 logged at cycletype=2CYC). Both the 0xB9-abort
and the cycle-type hypotheses are now dead. Bug is the G_CC_INTERFERENCE
two-tile combine / TEXEL1 path (IA8 smoke x RGBA16 fire @ tmem 0x188).

Probes (#ifdef PORT, GE_D172=1, inert otherwise):
- gfx_pc.cpp G_SETCOMBINE: distinct combine + active cycletype
- gfx_pc.cpp gfx_sp_tri1: tris with cyc2 combine drawn at cycletype!=2CYC

Needs a display capture to go further (headless can't see particle colour).
2026-09-08 23:12:09 -04:00
James Dansereau 809560ca1e docs(D172): withdraw 0xB9 root cause — it's G_SETOTHERMODE_L, already handled
0xB9 == G_SETOTHERMODE_L in this non-F3DEX2 build (G_IMMFIRST-6); fast3d
handles it as gsDPSetRenderMode. -level_09 sustained-fire run: zero D146
aborts, zero warnings. Re-scoped to the 2-cycle/cycletype hypothesis
(records never set G_MDSFT_CYCLETYPE -> use_2cyc==0 on PC -> TEXEL1
garbage -> magenta/cyan).
2026-09-08 23:12:09 -04:00
James Dansereau 90641f6305 Merge pull request #18 from jkdansereau/fix/d176a-sky-path-b
D176(a): render the sky on PC (Path B — screen-space emit)
2026-09-08 22:34:27 -04:00
James Dansereau b2efc0af9d fix(D176a): phase-fold sky texcoords to kill the horizon streak (Defect 1)
Cloud tile is s_skywaterimages[0] = 64x64 IA8, G_TX_WRAP -> it tiles.
GE's screen-space textured-quad idiom is tc = width<<5 (glass2.c:678), so
skyPortRenderPoly's *32.0f scale was correct; the bug was (s16) overflow on
the +/-20k-texel horizon S/T (unk20/unk24 are texel coords, not fractions).

Fix: fold every vertex of one primitive by the same multiple of the 64-texel
tile period before scaling. Inter-vertex deltas (hence the interpolated
cloud texture) are preserved; magnitudes stay in s16 range. Exact for any
triangle whose S/T span is < ~1000 texels -- everything except tris
straddling the horizon line, which are Defect 2 (adaptive tessellation).

sky.c #ifdef PORT only; N64 path unchanged. Builds clean; bunker1 + statue
verify PASS, no crash. UNVERIFIED VISUALLY -- owed: one -level_22 screenshot
(D176a-SKY-NOTES.md 'Verification ask').

Also: rebased onto main (porting-notes.md conflict resolved).
2026-09-08 22:29:28 -04:00
James Dansereau 9a6017a784 D176(a): emit screen-space sky geometry on PC (Path B)
skyRenderTri/skyRenderFull emit the sky as a verbatim N64 RDP triangle
command chopped into G_RDPHALF_* immediates, which fast3d deliberately
no-ops -> Surface/Statue/Streets skies render solid black.

Behind #ifdef PORT, substitute a screen-space gSPVertex + gSP1Triangle/
gSP2Triangles emit built from the already-projected SkyRelated38 verts
(new helper skyPortRenderPoly, ortho matrices via dynAllocate*). N64 path
unchanged under #else. Pure RSP-idiom substitution, no behavioural change.

Links 243/243. Render check owed on a display: -level_22 frame ~360.

findings.md D176(a) M-46; porting-notes.mdD.
2026-09-08 22:29:28 -04:00
James Dansereau b7bb08db65 Merge pull request #35 from jkdansereau/fix/d210-patrol-lastvisible-alias
fix(D210): chrToPatrol leaves act_patrol.lastvisible60 uninitialised on PC
2026-09-08 22:28:31 -04:00
James Dansereau 1a07c8201a Merge pull request #33 from jkdansereau/fix/d196-overlay-cursor
fix(D196): re-hide the OS cursor after the F10 options overlay closes
2026-09-08 22:28:28 -04:00
James Dansereau b47eea65e3 fix(D210): chrToPatrol leaves act_patrol.lastvisible60 uninitialised on PC
Sibling of D209 (A1 raw-byte union-alias class). chrToPatrol writes
act_init.padding[0x13] (union byte 0x4c) = -1, which is act_patrol.lastvisible60
on N64. act_patrol starts with a patrol_path* that widens 4->8 on x86-64, so
0x4c now lands in waydata and lastvisible60 (@0x50) stays uninitialised. The
patrol 'haven't seen the player recently' gate (chraction.c:~9347) then reads
garbage on the first ticks after entering PATROL.

Fix: write the named field under #ifdef PORT; N64 line verbatim under #else.
ABI/pointer-width only, no game-logic change.

Found by the post-D209 A1 sweep; the only new instance (act_anim/act_dead
padding aliases are pointer-free -> layout-stable).

Build clean; bunker1 + facility verify PASS (both patrol-heavy), no crash.
findings D210 + porting-notes A1 + findings-index regen.
2026-09-08 20:51:18 -04:00
James Dansereau a17d5466ae fix(D196): re-hide the OS cursor after the F10 options overlay closes
inputSuspendForOverlay() forces the OS cursor visible while the overlay is
open; on close nothing re-hid it unless reconcileGrab() happened to re-grab
(only true if the user had already clicked-to-lock in a stage). Opening the
overlay from a menu, or from a stage not yet click-locked, left the cursor
stuck visible over the game's own crosshair.

Fix: re-assert applyCursorVisibility() once per controller-0 poll, right
after reconcileGrab(). Unreachable while the overlay is open (early return
above), so it only fires once the overlay has closed and self-heals every
poll thereafter. Port-only, no render change; verify.sh bunker1 PASS.

Owed: 5-second visual check - open F10 from a menu, close it, cursor gone.
2026-09-08 20:24:35 -04:00
James Dansereau 97a09665a1 Merge pull request #32 from jkdansereau/diag/d193-timing-keystone-measured
fix(D209): AI locomotion locked to walk animation — union byte alias shifts at 64-bit (fixes D193)
2026-09-08 20:00:18 -04:00
James Dansereau b0b5f948f1 chore(D209): regen findings index
gen_findings_index.py --check gate was red on the new D209 entry.
2026-09-08 19:57:22 -04:00
James Dansereau a63efe184b fix(D209): AI locomotion locked to walk anim — union byte alias shifts at 64-bit
Root cause of D193 ("NPC AI locomotion is too slow", the user's biggest
playtest complaint; broke Cradle completability).

get_sound_at_range() (chraction.c:3573) is the locomotion-animation
selector: it picks ANIM_DATA_sprinting/running/walking from an arg1 speed
tier. Its only caller passed that tier as self->act_ubytes.padding[45] — a
raw byte alias into the ChrRecord action union rather than the named field.

On N64 offset 45 is exactly act_gopos.unk59:
  act_gopos = {coord3d targetpos@0, StandTile *target@12,
               waypoint *target_path@16, waypoint *waypoints[6]@20,
               u8 curindex@44, u8 unk59@45}
At 64-bit the three pointer members widen 4->8B (target@16, target_path@24,
waypoints@32), pushing unk59 to 81 while the literal 45 lands inside
waypoints[1] (40..47) — byte 5 of a heap pointer. The arena is low-4GB
(0x00000000_70xxxxxx), so that byte is always 0x00: arg1 reads 0 forever
and every AI chr in the game was bound to ANIM_DATA_walking /
ANIM_DATA_walking_unarmed regardless of what the ailist commanded. GE
character travel is animation-root-motion driven, so all AI moved at walk
pace on every level. Bond is unaffected (explicit physics, no root motion),
which is the reported "player fine / all AI slow" split.

Fix: PORT-only read of the named field; N64 line kept verbatim under #else.
ABI/pointer-width only, no game-logic change (rule-2 narrow exception, D3x).
Both callers reach the site with actiontype == ACT_GOPOS, so it is the same
byte N64 reads.

Evidence (Cradle -level_41, GE_D193A extended with tier/aidx): all six chrs
report ACT_GOPOS tier=1 SPEED_RUN on 420/440 samples, while the live
model->anim reverse-resolves to animation_table_ptrs1 index 40
(PTR_ANIM_walking) for Trevelyan and 107 (PTR_ANIM_walking_unarmed) for the
guards on every sample; 42 (running) and 41 (sprinting) never appear.
Travel sat in a flat 140-155 u/s band for all of them.

User playtest-verified on the fix build: Trevelyan and the guards run.

Also:
- GE_D193A extended with tier / gspd / typ / tick / move / frz / hid / aidx
  (aidx reverse-resolves model->anim against animation_table_ptrs1).
- bondtypes.h: unk59 was commented "guess: room" — it is the SPEED tier.
- porting-notes.md A1: new bug class — raw-byte aliases into a union read a
  plausible constant rather than garbage, so they misbehave silently.
- findings.md: D209 added; D193 closed against it.
2026-09-08 19:50:45 -04:00
James Dansereau 47d6dfa9c3 docs(D193): anim-data endianness (D33) audited complete; blocked on N64 reference
Static review of the full anim-root-motion path finds no port-side bug:
frame clock ruled out (measured), all model/chr scalars nominal,
ModelSkeleton ruled out (static C not serialized), and the D33
romdataFixupAnimationData byte-swap covers every field the locomotion
decode reads -- walk/run anim records ARE in animation_table_ptrs1,
_ptrs2 is 3 non-overlapping anims (no double-swap), no fixup errors,
sizeof(ModelAnimation)=64 and sizeof(ModelAnimBitField)=6 both platforms,
bitstream walked byte-MSB-first (endianness neutral, matches PD).

A magnitude error, if present, is only visible against an N64 reference.
Two paths documented (both need a fresh session; no MIPS toolchain /
emulator in this env): (A) build ROM with matching logging + diff,
(B) frame-count N64 reference footage of a measured guard traversal.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 10:03:16 -04:00
James Dansereau 33667c72d7 docs(D193): rule out ModelSkeleton (static C, not serialized) — bitstream/anim-table is the remaining surface
Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:51:14 -04:00
James Dansereau 25b46e020a diag(D193): instrument the locomotion / anim-root-motion path
Two #ifdef PORT env-gated probes (default-inert, no golden regression):

- GE_D193A (chr.c chrTick): per-wall-second per-chr travel speed +
  the root-motion scalars (scale / anim_translation_scale / animrate /
  playspeed / model->speed / g_GlobalTimerDelta). GE_D193A_CHR=<n>
  narrows to one chrnum.
- GE_D193B (model.c sub_GAME_7F06D3F4): rate-limited raw anim-bitstream
  root-motion decode (joint / mtx base channel / decoded s16 triple /
  angle / the D32 bitDescriptors+bitStream addrs).

Silo + Facility captures: every port-side input to the locomotion math
reads nominal -- g_GlobalTimerDelta=2.0, playspeed=1.0, scale=0.1,
anim_translation_scale=1.0, and the C in this path is identical to N64
(only the non-firing D156 NaN guards are #ifdef PORT). animrate=0.0 is
the default (only set on blended transitions). Patrolling guards travel
~60-99 u/s. GE_D193B shows base=0 / angle=0 on every call.

So the divergence is either subtle wrong magnitudes in the decoded
bitstream values, or in the pos34 accumulation -- neither visible
without an N64-decomp reference trace. Findings D193 + GE-ENV-PROBES
updated with the numbers and the next step (build n64decomp/007 with
matching logging and diff, or frame-count reference footage). One
explicit structural check flagged: ModelSkeleton.Joints (ModelJoint*,
4->8 B) reached via model->obj->Skeleton from D43/D45-converted data.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:50:06 -04:00
James Dansereau 7206bf8cc1 docs(D193): live-load confirms the pivot — locomotion/anim bug, not the frame clock
User ran Cradle (-level_41; the "-level_51" in the docs was wrong -> that
id is LEVELID_EAR, an unused slot that SIGSEGVs in stanBuildRoomData)
with GE_D193=1 and drove the Trevelyan sequence while Trevelyan walked
visibly slow. Probe held render=30 / sim=60 / clamped=0 / maxraw=2 dead
steady, no [D156] fires. So under the exact failing scene the sim clock
is perfect -> D193 is in the character locomotion/animation path.

Best hypothesis: GE character travel is anim-root-motion driven
(modelTickAnim -> modelSetAnimFrame2WithChrStuff, translation scaled by
model->scale * model->anim_translation_scale); the player uses explicit
physics instead, which explains "player fine / all AI slow". Next:
instrument sub_GAME_7F06D3F4 pos magnitude + scale + playspeed/speed per
chrTick for one walking NPC. Findings D193 + HANDOFF updated; Cradle
level-id corrected to 41.

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:38:07 -04:00
James Dansereau c723207d3a diag(D193): measure the timing keystone — "sim sums low" hypothesis contradicted
Add GE_D193=1 per-wall-second probe in waitForNextFrame (#ifdef PORT,
env-gated, default path byte-identical): render frames, sim ticks
delivered (Σ deltaFrames == Σ g_ClockTimer), catch-up clamp hits, max
raw pre-clamp delta.

Measured -level_09/_20/_27/_34 idle: rock-steady render=30 fps /
sim=60 ticks/s / clamped=0 / maxraw=2 on every level, VSync on or off.
The per-second sim sum is exactly the N64's ~60 and FRAMETIMING_PORT_
MAX_CATCHUP never bites in normal play, so B1's premise
(g_GlobalTimerDelta / g_ClockTimer sum low over a real second) is wrong
for the steady-state case. Findings D193 updated with the measurement,
the residual quantization risk (no remainder carry in the per-frame
round; only matters if a real AI scene drops into the 20-30 fps band),
and the revised next step (instrument under real AI load, then pivot to
the locomotion/anim path). GE_D193 catalogued in GE-ENV-PROBES.md.

No game-logic change; no golden regression (level_09 f203 91.52% vs
~91.6% ref).

Claude-Session: https://claude.ai/code/session_01U7gyYC3pcatXHhD1kPCgsR
2026-09-08 09:30:49 -04:00
James Dansereau 3eca8ba79b Merge pull request #31 from jkdansereau/fix/d191-bondview-cuff-switch-stride
fix(model): D191 — Bunker ii / Statue mid-mission crash (bondviewSelectCuff Switches stride)
2026-09-08 09:15:25 -04:00