Commit Graph
119 Commits
Author SHA1 Message Date
James Dansereau d0b17f1788 M-30 docs: D157 CONFIRMED by user playtest (Dam Agent -> save -> persist) 2026-08-31 10:50:33 -04:00
James Dansereau e610c57733 M-30 docs: D157 root cause (objective difficulty byte-offset) in HANDOFF + PORT-LEARNINGS 2026-08-31 10:26:02 -04:00
James Dansereau 2fde8a5d3f M-30: regenerate LEVEL-OBJECTIVES.md with fixed difficulty reader (D157) 2026-08-31 10:25:24 -04:00
James Dansereau 27c0544ce3 M-30 docs: narrow save blocker to objectiveIsAllComplete (Dam obj 0) 2026-08-31 09:35:46 -04:00
James Dansereau b22cd7469a M-30 docs: campaign-progression blocker + GE_SAVELOG diagnostic in HANDOFF 2026-08-31 08:49:14 -04:00
James Dansereau 385b792a0c M-30 docs: RC2 mip-texture fix + Y-invert fix in HANDOFF 2026-08-31 08:25:52 -04:00
James Dansereau ddef038238 fast3d RC2: clip mip-contaminated texture uploads to base height (+ GE_DTEX probe)
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.
2026-08-31 08:25:34 -04:00
James Dansereau b32a740986 M-30 docs: user batch defect list (#2 #3 #4 #5) in HANDOFF 2026-08-31 08:08:02 -04:00
James Dansereau b9c333d74f M-29 Track A: regenerate LEVEL-OBJECTIVES.md for all 21 solo levels 2026-08-31 07:44:03 -04:00
James Dansereau 290e85c50d M-29 docs/tools: SMALL-FIXES notes, LEVEL-OBJECTIVES decode, animgen dump helpers 2026-08-31 07:42:37 -04:00
James Dansereau 87d8e065d5 M-29 docs: D156 in HANDOFF 2026-08-30 21:45:58 -04:00
James Dansereau f887165ff4 D156: guard NaN/blown-up anim frame -> unbounded frame-step loop (Facility outro hang)
After D155 the user re-ran the playthrough and it hung again at the end
of Facility -- same stack but frames=12962 frozen CONSTANT for minutes =
a true infinite loop, not D155's slow spiral.

The `while (1)` at model.c:3131 in modelSetAnimFrame2WithChrStuff steps
root-motion accumulation one anim frame at a time from framea to frameb.
frameb comes from modelTickAnim's `frame += playspeed * speed` per tick.
A cutscene anim transition with a near-zero blend/timespeed/unkb0
denominator (model.c:3436/:3477, guarded >0 but not against tiny values)
makes model->speed/playspeed huge or NaN -> frameb huge/NaN ->
floorFloatToInt garbage -> endframe-curframe ~= 2^31 -> effectively
infinite.

#ifdef PORT guards (N64 unchanged):
- modelSetAnimFrame2WithChrStuff: if frameb non-finite or |frameb| >= 1e6,
  snap to framea (loop does ~0 iterations; modelConstrainOrWrapAnimFrame
  at :3211 still clamps model->frameb to a real frame).
- modelTickAnim: after the tick loop, if frame/frame2 went non-finite,
  fall back to the pre-loop model->animframe1/animframe2 so the NaN can't
  poison model state every frame.
`!(x > -1e6f && x < 1e6f)` catches NaN too. Anim frames are 0..few-hundred
so 1e6 is a safe ceiling.

Likely deeper cause (needs the repro): these speed/blend fields are Model
members -- a D100/D140-style struct-pun misalignment would feed garbage
exactly when a cutscene sets an unusual transition. Dump
model->speed/playspeed/timespeed/unkb0 in modelTickAnim during the
cutscene if it still hangs.

Verified: build green; -level_09 frame 200 unregressed (guards never fire
in normal play). PORT-LEARNINGS §E.
2026-08-30 21:43:39 -04:00
James Dansereau f33894f2a2 M-29 docs: D155 in HANDOFF 2026-08-30 21:37:47 -04:00
James Dansereau d8ca9a0c51 D155: clamp per-frame deltaFrames — fixes Facility outro-cutscene hang
User hit a "hang" (process alive, heartbeat dumps) at the end of the
Facility outro cutscene. Symbolicated mainThread stack: parked in
modelConstrainOrWrapAnimFrame <- modelTickAnim <- chrUpdateAnim <-
chrTick <- playerTick <- propsTick <- lvlRender across every dump.

Root cause: waitForNextFrame() (src/game/frametiming.c) passes
nextFrameTime = (osGetCount()-prev+387937)/775875 to updateFrameCounters
UNCLAMPED. osGetCount() is wall-clock on the port (D117), not VI-locked,
so a real-time stall (asset load at the cutscene->debrief boundary, made
worse by a local LLM hammering the box) makes nextFrameTime balloon to
hundreds/thousands. That becomes speedgraphframes -> g_ClockTimer, which
drives modelTickAnim's `while (numticks-- > 0)` per character per render
plus dozens of `for (i=0;i<g_ClockTimer;i++)` sim loops -> one frame does
thousands of anim ticks x N chars -> multi-second frame -> heartbeat
trips, and the slow frame feeds an even bigger delta -> unrecoverable
spiral. The N64 was VI-bound and never produced deltaFrames > ~2.

Fix: #ifdef PORT clamp nextFrameTime to FRAMETIMING_PORT_MAX_CATCHUP (6)
in waitForNextFrame(). N64 path verbatim. Timing-compensation class,
cf. D117/D134. PORT-LEARNINGS §E.

Verified: build green (ntsc-final); -level_09 unregressed (frame 200
nonclear delta 0.04pp, full-window crash-free). The clamp only engages
when a frame took >~100ms, so normal play is byte-identical.
2026-08-30 21:37:34 -04:00
James Dansereau eea9388217 M-29 docs: B1/B2 outcome in HANDOFF (3-point opt-in, B2 not filtering) 2026-08-30 21:10:11 -04:00
James Dansereau 944b28bd0d M-29: wire up opt-in N64 3-point texture filtering (BACKLOG B1 groundwork)
The sm64ex 3-point filter shader path already existed in gfx_opengl.cpp but
was never reachable from config, and its min-filter row was all GL_NEAREST
(no mip sampling) so it would shimmer badly.

- gfx_opengl.cpp: 3-point min-filter row -> GL_LINEAR_MIPMAP_{NEAREST,LINEAR}
  so minification uses the mips it already force-generates at upload;
  anisotropy default 0 -> 4 (0 was invalid for GL_TEXTURE_MAX_ANISOTROPY).
- video.c: Video.TextureFilter now accepts 2 = 3-point + trilinear.
  DEFAULT STAYS 1 (bilinear) -- 3-point is more console-authentic but
  softens textures at normal viewing distance, and (tested) did NOT fix
  the Depot roof garbage. That is a separate texture-data/UV/effect bug,
  not filtering -- BRIEF-B2 updated, next step is a GE_DTEX probe.

Verified: default (bilinear) -level_09 frame 320 unregressed vs golden
(nonclear delta 0.00pp, phash 11); build green; 1200-frame -level_30 run
crash-free with 3-point active.
2026-08-30 21:09:53 -04:00
James Dansereau 69e3670f2c M-29 docs: race-to-release plan, audio deferred, B2 Depot texture diagnostic
HANDOFF: M-29 session summary + the 3-track plan (A=playtest gate,
B=audio DEFERRED, C=solo quick wins in priority order).
BRIEF-B2-depot-textures.md: captured evidence + leading hypothesis that
the Depot roof garbage is mostly BACKLOG B1 (missing filtering/mips),
not a decode bug; decode path audited clean.
2026-08-30 21:01:53 -04:00
James Dansereau 52ec6d837c M-28: D154 (WIP, unverified) — port bg.c room-geometry hit-test GBI parser
bgTestRayIntersectionInRoom (bg.c) is the D135 sibling flagged since M-23:
an unported N64 GBI parser walking the 16-byte PC room DL with raw
((u8*)gdl)[k] / ((u32*)gdl)[i] 8-byte-Gfx byte/word indexing. It runs on
every shot that resolves against background geometry (bgTestBulletHitBackground),
so it AVs the first time you shoot a wall or floor in a real playthrough --
invisible to the no-input level sweep.

Ported #ifdef PORT (N64 path kept verbatim under #else):
 - vtxoff / vtxbase / op read through the .dma view, matching the
   already-ported bgBuildRoomVtxBounds.
 - G_TRI1 vertex indices from (w1 >> 16/8/0) & 0xff, /10.
 - the 12 G_TRI4 index nibbles recovered from (u32)gdl->words.w0/.w1,
   each mapped from the N64 byte/halfword layout (table in the code
   comment; cross-checked twice).
 - both texturenum recoveries -> -1: the KSEG0 *(u16*)(w1-8) deref is not
   valid for the port's converted GDLs (same as D135); texnum only selects
   the bullet-impact decal/sound, parked with D77.

Build: green (ntsc-final). NOT VERIFIED -- a no-input GE_PCDUMP capture
never fires a bullet so it never enters this function; needs a firefight
into a wall on an idle machine. This session's box was too thrashed to get
a clean run (D153 driver-crash-under-load). Committed as WIP so the port
work isn't lost; next session: idle-machine wall-shoot test + -level_09
framediff, then mark verified.

§F/HANDOFF D154, PORT-LEARNINGS §B (D135 corollary).
2026-08-30 20:34:15 -04:00
James Dansereau 57e24a374b M-28: watch-menu text (D150/D151) + audio-lock deadlock mitigation (D152)
D150 (src/str.c) — NULL-tolerant strcpy/strncpy/strcat under #ifdef PORT.
The watch BRIEF/OBJECTIVES pages build text with strcat(buf, langGet(id));
langGet() returns NULL on PC for a string bank the in-level flow never
loaded, and str.c's primitives are __nonnull__ builtins so a plain param
NULL check is optimised away — the guard launders the ptr through an empty
asm (GE_IS_NULL). NULL src -> empty string instead of an AV crash.

D151 (src/bondtypes.h) — watch data (objectives / mission background / M /
Q / Moneypenny) all rendered blank. propDef types 35 (WatchMenuObjectiveText)
and 23 (ObjectiveStart) store a plain s32 lang slot id in their 3rd word;
the decomp structs decode it as [u16 reserved][u16 text] reading text at
offset 0xA, which only works because the id is in the low 16 bits of a
big-endian word. d88_propdefs.py bswap32's the whole word -> text @0xA
reads 0 -> langGet(0) -> NULL. Fix: text is a full u32 at the word offset
under #ifdef PORT (N64 layout kept under #else). No converter change, no
sidecar regen. This is the D143/D150 "briefing text renders blank" root cause.

D152 (port/src/libultra.c) — mission-failed / objective-failed permanent
black screen. The D147 recursive-mutex behind osSetIntMask can be leaked
(libaudio has unbalanced / early-return mask calls; a transient thread can
acquire OS_IM_NONE and exit) -> mainThread + amMain both block forever in
alEvtq* -> gfx pipeline stalls. Live gdb.txt from the user's session
confirmed the deadlock (mainThread in the mission-fail audio fade-out
sndSetScalerApplyVolumeAllSfxSlot -> alEvtqPostEvent). Mitigation: model
the mask as a self-healing logical lock (owner/depth under s_imMx + condvar)
— normal microsecond critical sections behave as before, but a waiter
blocked > 2s concludes the holder leaked it, logs the stale owner + the
stealing caller's return address, and steals the section. Worst case is a
~2s audio hiccup + a log line naming the leak, instead of a hung game.
Proper narrow fix (dedicated ALEventQueue lock) still owed.

D153 investigated and dismissed — the "-level_09 crashes at frame ~900"
seen while regression-testing was D117/D134 host-scheduling flakiness under
heavy concurrent build+run load; on a quiet machine -level_09 runs past
frame 2400 clean with all three changes applied.

Build: green (ntsc-final). Golden framediff not run clean (same machine-load
flakiness); one quiet -level_09 run = 2400+ frames, 0 heartbeats.
2026-08-30 20:25:02 -04:00
James Dansereau 81a0c0d253 M-27 docs: HANDOFF — full front-end loop DONE (D142-D147), test tooling, D148/D149 parked 2026-08-30 18:52:30 -04:00
James Dansereau 84516ad69b M-27 docs: D142/D147 bug classes (PORT-LEARNINGS D3/D4), D148 Dam cutscene + D149 front-end model in GRAPHICS-BACKLOG 2026-08-30 18:51:35 -04:00
James Dansereau f98414f8c6 M-27: front-end bucket — D140/D141/D118d pause+watch menu fixes
D140 (src/game/bondview.h, bondview2.c): pause-menu crash in
bondviewRenderWatch -> bondviewTransformManyPosToViewMatrix(field_23C=NULL).
player +0x230 is a struct Model + RW pool punned into a 0x184-byte field run;
field_23C/watch_scale_destination/pause_watch_related_adjust alias
.render_pos/.scale/.animframe1. PC sizeof(struct Model) grows -> aliases
break -> NULL render_pos. Fix: real inline struct Model + u32 watchRwPool[192]
under #ifdef PORT; named reads redirect via GE_WATCH_* macros (N64 #else
verbatim).

D141 (src/game/gunfire.c): crash D140 exposed.
set_enviro_fog_for_items_in_solo_watch_menu walks bodymodel->Switches[]
(ModelNode* array) with raw byte offsets +0x48/+0x5c, j+=4 -> PC 8-byte
stride reads garbage -> AV in modelGetNodeRwData. #ifdef PORT uses
Switches[18 + (j>>2)] / Switches[23 + (j>>2)].

D118d (src/game/options.c): watch inventory list over-scrolls with keyboard
W/S (one press skips several items). GE fast-scroll is a raw per-frame level
check (joyGetStickY < -0x46); digital pads sit at max every frame. #ifdef
PORT drops the stick term -> nav via the latched single-step path. Feel
unverified headlessly.

Verified: build green (ntsc-final); -level_09 framediff 3/3, -level_20
crash-free (unregressed).
2026-08-30 14:14:41 -04:00
James Dansereau 326564736d M-26: [D] [Debug] ini flags mirroring GE_PCDUMP / GE_INPUTLOG
- config.c: Debug.FrameDump (string) + Debug.InputLog (int).
  configGetFrameDump()/configGetInputLog() check the env var first
  (tools_pc/*.sh unaffected), fall back to the ini.
- video.c / input.c consumers switched to the accessors.
- GE_DETERM is design-only (D117), not mirrored.
- Verified: ini-only FrameDump=40-120:20 -> exactly 5 frames; GE_PCDUMP
  still overrides; -level_09 crash-free.
2026-08-30 13:00:52 -04:00
James Dansereau 710d02f802 M-26: [B/C] level_sweep STALLED verdict, raw mouse input opt-in
- level_sweep.sh: frames rendered then frozen with process alive ->
  STALLED (froze at N/M frames) instead of a false PASS (the Silo
  -level_20 fly-down / D117-D134 load case). Retried once like NO-FRAMES.
- input.c: Input.MouseRawInput=1 (default 0) sets SDL relative
  system-scale + warp hints off in inputInit -> no OS pointer accel on aim.
- docs: M-26-QOL-REVIEW.md + HANDOFF section.
2026-08-30 12:55:12 -04:00
James Dansereau 905eebf447 docs: Silo -level_20 freeze was machine-load flakiness (D117/D134), not a real bug 2026-08-30 12:47:47 -04:00
James Dansereau aafa86d180 docs: M-25 QoL session summary + re-sweep (20/21, Silo freeze clarified) 2026-08-30 12:46:07 -04:00
James Dansereau 39dded2a17 M-25: [Input] ge007.ini knobs — mouse Y scale/smoothing, pad deadzone/trigger/invert-Y 2026-08-30 12:35:17 -04:00
James Dansereau bc97cdab0a M-25: playtest.sh — accept level names, --list, auto-symbolicate crash PCs 2026-08-30 12:31:08 -04:00
James Dansereau f6006bcdb4 M-25: [Video] ge007.ini knobs (vsync/fps cap/MSAA/filter/fullscreen) + config auto-migration 2026-08-30 12:30:00 -04:00
James Dansereau 645a301b41 docs: M-24 QoL review sheet (M-24-QOL-REVIEW.md) + cross-links 2026-08-30 12:21:17 -04:00
James Dansereau 9d136bc657 M-24: more playtest QoL — gamepad hotplug, wheel weapon-cycle, title FPS
port/src/input.c:
- inputRescanPads(): close + re-open all pads on hotplug. Game latches the
  connected mask at osContInit so a late pad merges into controller 0.
- inputPostWheel(): a wheel notch queues a 2-poll A-button press. GE's
  default scheme cycles weapon forward on a bare A edge (invButtons=A,
  bondview2.c:5162/5326). Both directions cycle forward.

port/src/video.c: pump SDL_MOUSEWHEEL -> inputPostWheel,
SDL_CONTROLLERDEVICEADDED/REMOVED -> inputRescanPads, and refresh the
window title with vidAvgFPS ~1 Hz via wmAPI->set_window_title.

-level_09 2/2 crash-free to frame 900, unregressed (91.6%). Build green.
2026-08-30 12:18:55 -04:00
James Dansereau 46fa579e54 M-24: playtest QoL — F12 screenshot + mouse frees on focus loss
port/src/video.c: F12 sets a request flag consumed in videoEndFrame (render
thread, GL context current) -> ppm/shot_NNN.ppm via the existing
gfx_opengl_dump_bound_fbo. SDL_WINDOWEVENT_FOCUS_LOST/GAINED now call
inputSetMouseGrab(0/1) so alt-tabbing away releases the trapped cursor.

port/src/input.c: add inputSetMouseGrab() — toggles SDL relative-mouse
mode (respecting MouseEnabled), zeroes the aim delta, and gates inputUpdate
mouse reads on a mouseGrabbed flag.

-level_09 3/3 runs crash-free to frame 900, unregressed (91.6%).
2026-08-30 12:08:22 -04:00
James Dansereau 546d803870 M-24: mouse-look rework (D118b/c) + real ge007.ini parser
port/src/input.c: mode-aware mouse->pad map matching bondviewProcessInput.
Aim mode (RMB held) drives the analog stick past +/-60 for proportional
yaw+pitch and emits no C-buttons; hipfire keeps digital C-up/C-down pitch.
Fixes D118b (mouse-Y inverted) and D118c (aim + mouse-down -> crouch, from
C-down being overloaded to crouch/lean/zoom in insightaimmode). D118a
(hipfire digital pitch vs analog yaw) residual, minor. Port-layer only.

port/src/config.c: implement configLoad/configSave (were TODO stubs).
Parses $S/ge007.ini ([Section] blocks, Key = value, #/; comments), clamps
ints to registered bounds, writes defaults on first run. New knob
Input.MouseTurnSpeed (hipfire yaw %) split from MouseAimSpeed.

Verified: -level_09 crash-free 900+ frames @ 91.65% (unregressed),
-level_20 crash-free, ini round-trips with no unknown-key warnings.
2026-08-30 11:59:52 -04:00
James Dansereau e9cf24ce7d docs: M-24 next-task — park D139/D140 in front-end bucket, in-level playtest first 2026-08-30 11:35:46 -04:00
James Dansereau 82e118a218 docs: HANDOFF M-23 committed-state fixup 2026-08-30 11:29:02 -04:00
James Dansereau 73820a0c24 M-23: WS6 Facility playtest — D135/D137/D138/D139 (firefight, aim, freeze, unload)
First real human WS6 playthrough. Each crash is a subsystem the 21-level
boot sweep never exercises.

- D135 (propobj.c) bgTestHitOnObj: unported N64 GBI parser — raw 8-byte-Gfx
  byte/word indices desync on the PC 16-byte Gfx stream, walk off the DL.
  Ported #ifdef PORT to gdl->words.w0/.w1 shifts (mirrors pd_port bg.c:3635).
  Sub-fix: objHit:9717 div-by-0 — texnum recovery not portable, force -1.
- D137 (gunfire.c) gunDrawSight: s32 sp54 holds a Gfx*; 4-byte slot splices
  adjacent stack into the pointer -> wild DL write on right-mouse aim.
  #ifdef PORT: Gfx *sp54.
- D138 (snd.c) sndCreatePostEvent: parked audio never drains evtq->allocList,
  per-objTick positional-sound posts stall the main thread ("1 room from the
  end" freeze). #ifdef PORT no-op until audio lands (D77).
- D139 (cleanup_objects.c) cleanupObjects: read the propDef type via
  (u8)obj[0] — correct only big-endian (type = low byte of the header word).
  On LE that's extrascale -> walk never hits PROPDEF_END -> runs off the blob
  -> frees garbage -> stage-unload crash. #ifdef PORT uses pdef->type.
  UNVERIFIED (fast teardown test hit D140 first).

Open: D140 pause-menu watch-model NULL matrix; D118c aim-mode mouse-down
-> crouch. New harness tools_pc/repro_gdb.sh + build-pc/d136_cmds.txt.

-level_09 unregressed (91.65%). Docs: PCPortResearch §F D135-D140,
PORT-LEARNINGS §B/§C, HANDOFF M-23 + M-24 next-task.
2026-08-30 11:28:50 -04:00
James Dansereau 78c0f72157 level_sweep.sh: early-exit watchdog; M-22 re-sweep 20/21 PASS
The 45s watchdog is now a ceiling, not a flat per-level cost: a level is
killed as soon as its whole GE_PCDUMP frame set is on disk (PASS) or the
ppm count has been frozen for STALL_SECS (default 18). EXPECT_FRAMES is
derived from SWEEP_DUMP (start-end:stride). Full 21-level sweep ~7min
(was ~16).

M-22 full re-sweep after D134: 20/21 PASS. Facility(34) + Jungle(37) --
the M-20 re-verify failures -- both PASS clean, confirming D134 was their
cause. Only Cuba(54) crashes: known D129 residual (bare -level_54 hits the
cast/credits scroll referencing an unloaded text bank), parked with D76.
2026-08-29 23:57:03 -04:00
James Dansereau 46d4f03e5a D134: task-done SP/DP events must not be dropped into a full sched interruptQ
The frame-2 boot hang blamed on D117 flakiness since M-13 is a real bug:
osSpTaskStartGo posted OS_EVENT_SP/DP with OS_MESG_NOBLOCK into the 8-slot
scheduler interruptQ that the 60 Hz VI pacemaker also fills. A gfx task runs
the whole frame synchronously on the sched thread, so a slow early frame lets
retraces fill the queue and the done event is silently discarded -> curRSPTask
never clears -> pendingGfx never clears -> permanent stall.

portPostEventForce() drops the oldest (stale retrace) message and retries;
portPostVIEvent reserves 2 slots. -level_09: 6/6 boots to frame 600, 0
heartbeats (pre-fix 1/3).

Also hardens tools_pc/level_sweep.sh: 45s watchdog, lockdir serialization,
stray-process kill, one retry on NO-FRAMES.
2026-08-29 23:32:17 -04:00
James Dansereau 577f32f01f docs: M-21 — D132 applied + verified, handoff points to WS6 2026-08-29 23:16:43 -04:00
James Dansereau 5e9ea50add D132: fix propDef union-index slots for LINK/SWITCH/LOCK_DOOR/SAFE_ITEM
Types 14/19/38/44 declare their Index{1,2,3} fields as a union with a
pointer, so on PC each is an 8B/8-aligned slot (Index1@8, Index2@16,
...) and the record ends in a *next the proplvreset2 walk writes. The
converter was laying the N64 tight-4-byte index words at PC bytes 4/8/12,
putting Index1 where the compiler reads Index2 and leaving Index2 zero.
Non-crashing but silently broke switch-doors, dual weapons, padlocked
doors, and safe/safe-item links.

- tools_pc/d88_propdefs.py: PROPDEF_PC_BYTES 14/19->32, 44->40; handler
  emits Index{k} into low 4B of the 8-aligned slot at PC 8+8*k.
- loadobjectmodel.c sizepropdef() PORT: LINK/SWITCH/LOCK_DOOR->8,
  SAFE_ITEM->10, TAG stays 6.

Verified: -level_09 framediff 3/3 PASS; -level_20 identical to a
freshly-built pre-D132 baseline (frame_000320 delta is pre-existing
stale-golden/Silo slowdown, not a regression); Archives/Streets/Dam
load+render+no-crash.
2026-08-29 23:16:16 -04:00
James Dansereau e5f4290bc6 docs: M-20 — Facility + Jungle re-verified PASS on idle machine (concurrent-load flakiness, not regressions) → 21/21 2026-08-29 22:47:30 -04:00
James Dansereau 5f985e9756 docs: M-20 — D132 propDefs union-index layout divergence + D133 intro-render triage
D132 (propDef converter layout audit, static): converter cursor confirmed
correct vs real PC struct layout for every propDef type the 21 solo levels
emit (DOOR/ObjectRecord-prefix/VEHICHLE/AIRCRAFT/TANK/AUTOGUN/AMMO/
TINTED_GLASS/objective sub-records all MATCH; handler set is total). Real
divergence found for types 14/19/38/44 (LINK/SWITCH/LOCK_DOOR/SAFE_ITEM):
s32 IndexN fields share a union with a pointer -> 8B/8-aligned slot on PC,
converter emits at N64 tight 4B offsets -> indices misread -> switch-doors /
dual-weapons / locked-doors / safes silently fail to link (non-crash,
WS6 blocker). Proposed fix in D132, NOT applied - needs regen + game verify.

D133 (intro render triage): intro 'mostly black' is NOT a regression - it is
the D75/D76 parked-cosmetic steady state. Legal-screen coverage pixel-
identical (6677 non-clear px) between M-17 7159fff5 (worktree build) and
HEAD. Parked. Facility -level_34 + Jungle -level_37 re-verify failed this
pass -> flagged RE-VERIFY in LEVEL-STATUS, not investigated.
2026-08-29 22:37:11 -04:00
James Dansereau 71c205092a docs: M-20 — intro "mostly black" is D75/D76 steady state, not a regression
Built M-17 (7159fff5) in a scratch worktree, captured the same
GE_PCDUMP="20-900:20" window: coverage is pixel-identical to HEAD
(legal screen 6677 non-clear px / 2.17% on both builds). The 2D/text
layers draw; animated character-model layers never appear = open D75.
The M-17 "entire intro renders" handoff line was aspirational.

- HANDOFF: reworded Next-task items 2/3 (intro parked, not a regression);
  item 1 (BUNKER1 boot crash) already disproven by coordinator.
- LEVEL-STATUS: Facility -level_34 (boot crash 0x1400c3b77) and Jungle
  -level_37 (frame-2 hang) failed re-verify this pass on a lightly-loaded
  machine; flagged RE-VERIFY pending a clean-machine re-check.

PCPortResearch.md §F (D133) + PORT-LEARNINGS.md §E left uncommitted —
tangled with a parallel static-audit agent's in-flight D132 edits.
2026-08-29 22:36:30 -04:00
James Dansereau 106323aeaa docs: M-20 — BUNKER1 regression disproven (re-verified PASS); fresh Qwen static-audit tasks
- LEVEL-STATUS: -level_09 boot crash from M-19 did not reproduce on ef1ec523
  in 3 runs (1800 frames crash-free). Was sweep flakiness / D117 nondeterminism
  under machine load, not a real regression.
- HANDOFF-LOCAL-AGENT: replaced the 2 completed M-19 tasks with 3 fresh
  static-only audits (osVirtualToPhysical D131 corollary site sweep, in-place
  relayout D130 corollary sweep, glyph-index OOB audit).
2026-08-29 22:26:52 -04:00
James Dansereau bb14baf3bf @
D131: restore truncated module ptr in seg_addr (Jungle -level_37)

explosionRenderPropSmoke passes a compiled .bss matrix symbol
(&dword_CODE_bss_8007A100) through osVirtualToPhysical() -- a u32-returning
shim -- into gSPMatrix, truncating the 0x1_00000000 module high word. w1
lands as 0x40xxxxxx and seg_addr() returns it raw -> wild deref in
gfx_sp_matrix when the first explosion draws (~frame 300).

Fix: seg_addr() restores the module high word for any fallthrough w1 in
[0x40000000, 0x70000000) (module fixed-based at 0x140000000, no ASLR;
DRAM/KSEG0/segmented/phys are all handled in earlier branches). Covers the
~30 latent osVirtualToPhysical(<compiled matrix/vtx symbol>) sites in
explosion/glass/blood/bondview2.

Verified: Jungle renders to frame 1500-2400+ crash-free at 91.7% content;
-level_20/-level_24 unregressed. Sweep 18->19/21 (note: -level_09 has a
separate pre-existing boot crash regressed since M-18 -- see HANDOFF).
@
2026-08-29 22:13:08 -04:00
James Dansereau 1c4efdaaf2 docs: audio plan (Phase 3 draft), texture glitch analysis, BACKLOG graphics-settings expansion
- AUDIO-PLAN.md: draft Phase-3 libaudio->SDL plan (D77), Acmd-stream
  execution + SDL device plumbing; supersedes the §6 audio paragraphs.
- TEXTURE-GLITCH-ANALYSIS.md: read-only diagnosis of texture artifacting
  (G_SETTEX no-op, mip-chain contamination, wrap period, palette
  off-by-one); fixes pending.
- BACKLOG.md: PC graphics settings workstream expanded with PD-parity
  baseline + stretch goals, config/video starting-point notes.
2026-08-29 21:34:00 -04:00
James Dansereau fda4c19159 D130: stage fontchar fields before in-place 24->32B relayout (C2 Facility+Runway)
romdataFixupFont re-lays out the N64 24B fontchar array into the PC 32B
array in place; for glyphs 0/1/2 dst overlaps src by less than the 24B
read span, so a forward field write clobbered a later field's source
mid-loop (glyph 1 width = bswap(index), glyph 2 pixeldata -> wild ptr).
Facility's level title 'Chemical Warfare Facility #2' rendered '#' with
the wild pixeldata -> gDPLoadTextureBlock -> fast3d AV in import_texture_i8.

Fix: stage all six N64 fields in a local f[6] before writing any.
Not the model-GDL relocation the D124-Facility addendum suspected
(disproved; D124 entry + BRIEF-C2gdl-model-reloc premise corrected).

Verified: -level_34 0/~14 runs, -level_35 0/4 (was ~3/4);
-level_09/20/24 unregressed. Sweep 18 -> 20/21 PASS; only Jungle C2m
(gfx_sp_matrix explosion-DL G_MTX) remains.

Docs: D130 in PCPortResearch §F/§H, LEVEL-STATUS 20/21, HANDOFF M-18,
PORT-LEARNINGS §A (in-place relayout aliasing + negative glyph index).
2026-08-29 21:33:56 -04:00
James Dansereau 7159fff544 docs: M-17 session — D126/D127/D128/D129, 13->18/21 levels pass
LEVEL-STATUS: Bunker2/Depot/Surface2 (D126), Surface1 (D127), Control
(D128) now PASS. Only C2/C2-GDL remains (Runway intermittent, Facility,
Jungle). Cuba loads+renders but bare-boot end-credits path still faults
(D76/D129). HANDOFF next task = C2-GDL model relocation (own session).
PORT-LEARNINGS: D126 corollary (trailing runtime list ptr in ROM record),
D128 (raw hardcoded N64 struct stride bites the same as Gfx/Vtx sizing).
2026-08-29 20:27:20 -04:00
James Dansereau 73dfa8d81f D126: objective sub-record ->next pointer growth desyncs propdef walk
criteria_picture (30) / criteria_roomentered (32) / criteria_deposit (33)
/ setup_objective_text (35) each end in a T*next list pointer that the
setup walk writes unconditionally (set_parent_cur_obj_* /
setup_briefing_text_entry_parent). On x86-64 the pointer widens 4->8B and
8-aligns at offset 16, making the struct 24B/6w (N64 16/20). d88_propdefs.py
emitted N64 size -> the runtime 8-byte ->next store clobbered the next
propdef record's header -> walk desync -> command indices drifted ~100 ->
setupDoor linkedDoor resolution landed on the wrong record -> crash.

Fixes C3r (Bunker2 -level_27), C4 (Depot -level_30), C6 (Surface2 -level_43)
in one change. Sweep 13 -> 16/21. Bunker1/Silo/Aztec/Archives/Egypt/Train
unregressed; d125_check.py 21/21 MATCH.

- tools_pc/d88_propdefs.py: PROPDEF_PC_BYTES[30/32/33/35]=24 + typed handler
- src/game/loadobjectmodel.c: sizepropdef PORT returns 6 for those types
2026-08-29 19:35:19 -04:00
James Dansereau 060a7252e5 D125: fix d88_emit.py:374 8-byte slice / 4-byte literal (Aztec C3 crash)
emit_pad()'s widened-stan zero-fill assigned a 4-byte literal to an
8-byte bytearray slice. Python slice-assign with a short RHS silently
deletes the surplus bytes, so `out` shrank 4 B per pad+boundpad; once
len(out) dropped below a later leaf write's target offset, the write
clamped to empty-at-end and inserted there, drifting/truncating the
boundpad plink-name blob (Aztec pad33 "p138d2" -> "8d2") -> stanPackId
reject -> NULL stan -> door model=NULL -> crash propobj.c:13601.

Fix: 8 NUL bytes (only width-mismatched out[ write in the file).
All 21 Usetup*Z sidecars regenerated. Aztec (-level_28) now PASSES
(was C3 CRASH); Bunker1/Silo unregressed; d125_check.py 21/21 MATCH.
Sweep 12 -> 13/21.

Bunker2 (-level_27) still crashes: separate DOOR-tail linkedDoor
layout bug in d88_propdefs.py (C3r) -- linkedDoor is resolved from
linkedDoorOffset (read-before-write int id, D123 pattern). Folded
into the converter write-audit.

M-16 probes reverted; d125_check.py kept as the offline regression guard.
Generalisable quirk added to PORT-LEARNINGS.md §C.
2026-08-29 19:12:52 -04:00