Commit Graph
94 Commits
Author SHA1 Message Date
James Dansereau 8fdf473dc8 D116: probe finds HUD mirror is per-quad texture-U flip, not proportional-font-specific; ammo digits also mirrored; re-opens D114 2026-08-29 10:31:07 -04:00
James Dansereau 59d49a5e3f harness: CLAUDE.md auto-loads AGENTS.md + session/dispatch preflight checklists 2026-08-29 10:20:52 -04:00
James Dansereau a86868221d docs: D116 HUD text mirror is per-glyph texture-space flip, not screen mirror 2026-08-29 10:04:37 -04:00
James Dansereau 7bdb19e96f D115: fix gunfire THROW* raw struct player offsets; M-6 D113/D114 findings
D115: THROWMTX/THROWPOS/THROWPREV in gunfire.c were raw byte offsets
((u8*)g_CurrentPlayer + handnum*sizeof(struct hand) + 0xAD8/0xB08/0xB48).
Both the stride (PC sizeof(struct hand) ~0x968 vs N64 0x3B8, D102) and the
base offset are N64-sized, so on x86-64 the address lands inside the
inline gait Model / bondhead-matrix region of struct player and
matrix_4x4_copy(THROWMTX,..) scribbles 64 bytes of live render state on
every casing-ejecting shot. The offsets are exactly
hands[handnum].throw_item_pos_related[_prev] and its translation row -
macros now use those field accessors under #ifdef PORT; N64 build
unchanged. Full survey in docs/AUDIT-M6-player-offsets.md (10 sites,
3 HIGH fixed here, 2 MED open incl. the likely 'weapon model doesn't
draw' cause).

D113 (investigation, no code change): the 'portal-BFS under-reach / void'
premise is stale - the BFS output is correct for BUNKER1's topology
(rooms 26/27 are behind PORTALFLAG_DISABLED closed-door portals, culled
identically on N64). The void is the closed-door models obj 153/154 not
rendering in their doorway -> D114 track.

D114 (investigation, no code change): joint-matrix chain + d43_emit.py
float emission verified clean post-D112; basemtx is a proper
right-handed +Y-up lookat, joint render_pos coherent and upright. All
plan hypotheses disproven. Residual = a horizontal mirror in the shared
port/fast3d viewport / MP-matrix path vs GE's left-handed screen
convention (also explains the open D75 mirrored-text / logo items).
Still open.

Plan + agent reports: docs/PLAN-M6-playable.md. §F: D113/D114/D115.
2026-08-29 09:53:00 -04:00
James Dansereau 64698f6b23 docs: subagent workflow + port-learnings cheat-sheet
Distilled from Chris Judnich's 'Decompiling a Nintendo 64 Game in 84 Days'
(saved under docs/reference/): explicit per-task budgets exposed to the
agent, a shared append-only learnings file, file-partitioned parallelism,
cheap pre-flight passes, and a standard investigation-subagent brief
template. PORT-LEARNINGS.md is a one-screen index of the recurring
N64->PC bug classes into PCPortResearch.md §F; AGENTS.md points to both.
2026-08-29 09:52:47 -04:00
James Dansereau 3a02ded079 D112: fix d43_emit put_f32 byte-swap off-by-one (skeletal models collapsed)
put_f32 reversed bytes doff+1..doff+4 instead of doff..doff+3, dropping
each float's MSB and pulling in a byte of the adjacent field. Corrupted
every f32 in converted model rodata (joint Origins, LOD distances, BSP
planes, bounding radii) for all sidecar-loaded models. Compiled-in
front-end models were unaffected, hence 'logo fine, every guard a line'.

render_pos[] joint matrices now coherent; BUNKER guards render as
humanoids and monitor screens draw content. Regen: python
tools_pc/d43_emit.py ntsc-final.
2026-08-29 09:21:12 -04:00
James Dansereau cb73ea0985 docs: D107 mip-tile fix; storage void root-caused as closed-door props not rendering (prop/model track, not visibility) 2026-08-28 22:18:01 -04:00
James Dansereau 1fe85339f2 D107: fast3d sampled unloaded mip tile for GE's LOD textures (blurry ceilings)
GE's room GDLs emit G_TL_LOD + G_TD_DETAIL for mip-mapped textures, but
gfx_detail_textures_enabled is false for this port. gfx_lod_tile_offset's
'rdp.tex_lod ? rdp.tex_detail : i' then returned tex_detail (1) for every
texel, so fast3d sampled GE's first mip (render tile 1). GE loads the
whole mip chain with one G_LOADBLOCK to TMEM 0 and just points tiles 1..5
at offsets inside it; fast3d keys loaded_texture[] by TMEM addr and has
no entry for tile 1's offset, so import_texture fabricated a 16x16 from
the top-left 256 bytes of the base image and magnified it across the
polygon -- the blurry brown/grey ceilings and wall panels in BUNKER1.

Fix: when detail textures are disabled, gfx_lod_tile_offset always
returns 0 (the base render tile, the only correctly-loaded level). Sharp
near surfaces, matching what the N64 shows at close range.

Verified -level_09: BUNKER1 ventilation room ceilings/walls/light panels
now render crisp instead of brown mush. Single-level textures (racks,
room 1) were already fine and are unaffected.
2026-08-28 22:02:25 -04:00
James Dansereau 5f6741a432 docs: D106 portal near-plane fix; residual BUNKER1 render items (bounded sky gap + blurry texture) 2026-08-28 21:44:02 -04:00
James Dansereau ad220f4e3d D106: portal near-plane projection garbage no longer culls the next room
BUNKER1's portal-visibility BFS (sub_GAME_7F0B7F84 -> sub_GAME_7F0B5864)
projects each candidate portal polygon to a screen-space bbox. When the
player straddles a portal's plane, sub_GAME_7F0B5528 emits z==0 near-plane
clip points; transform3Dto2DWithZScaling then uses inv_z = -1e20, sending
those points to +/-1e20-scale screen coords. On N64 the two clip points
bracket the view symmetrically, so the degenerate-box check (bg.c:1713)
or the downstream screensize clamp yields a full-screen box and the room
is kept. On x86 the exact garbage can come back min>max on a single axis
(or non-finite), slipping past that check -> the portal is dropped and
the room the player is walking toward vanishes, showing skyRender's
background fill through the doorway.

Fix: #ifdef PORT, treat any wildly out-of-range / non-finite projected
bound as degenerate -> full screensize (matches the N64 outcome).

Verified -level_09: per-frame visible-room count 1-3 -> 2-4; rooms beyond
near doorways (r11 from r27, etc.) now render. Reworks the GE_D104 probe
to dump the per-frame visible-room list (gated, capped).

Residual (separate): a bounded central sky gap + a blurry grey wall/
ceiling texture remain in the storage rooms — a dropped-poly / texture-
dimension issue in the D85 room GDL path, not visibility.
2026-08-28 21:43:24 -04:00
James Dansereau 5a5c73de56 D105: emit G_CLEAR_DEPTH_EXT — depth buffer was never cleared in-level
zbufClearCurrentPlayer (viewport.c) clears the N64 Z buffer with the
'point the colour image at the Z buffer and gDPFillRectangle it' idiom.
fast3d does not emulate that: gfx_dp_fill_rectangle bails when
color_image_address == z_buf_address assuming glClear already cleared
depth, but the frame-start clear_framebuffer(true,false) is colour-only.
fast3d expects G_CLEAR_DEPTH_EXT (0x44) for a real depth clear and GE
never emits it. Here the two addresses don't even match
(gDPSetDepthImage(z_buffer & ~0x3F) vs gDPSetColorImage(K0_TO_PHYSICAL))
so the packed-Z fill colour is scribbled onto the framebuffer instead.

Net: every in-level frame rendered against a stale/garbage depth buffer,
all ~160k room triangles failed the Z test, only skyRender's background
fill survived -> flat dark-blue screen + HUD.

PORT branch in zbufClearCurrentPlayer emits a bare G_CLEAR_DEPTH_EXT and
returns, skipping the N64 idiom (a no-op / harmful in fast3d anyway).

Verified -level_09: 90% flat sky-colour -> 2585 distinct colours, sky
16%; recognisable BUNKER1 (textured walls, storage racks, floor). 70s
crash-free. Adds GE_D104 room-render-pass probe in bg.c (gated, capped).

Remaining: dark-blue wedge (geometry gap) + brown band top third
(ceiling tex); then struct player weapon/offset pass; then D75(b).
2026-08-28 21:06:37 -04:00
James Dansereau 82f71855ca D103: native-res height from yScale, not hard-coded 480 (viewport was half-height)
osViSetMode hard-coded the fast3d native resolution to the visible
scanline count (pal ? 400 : 480) instead of the 320x240 CFB space GE
authors its GBI in. SCREEN_WIDTH/SCREEN_HEIGHT alias the native viewport;
RATIO_X came out 2.0 but RATIO_Y 1.0, so every gSPViewport/G_SETSCISSOR
was a 220px band (~46% of frame) shown in the lower half after invert_y.
gfx_current_native_aspect was wrong too (320/480).

Recover CFB height from vm->fldRegs[0].yScale (bufy*0x800/480, fr.c:417)
with a pal ? 272 : 240 fallback. comRegs.width already carried bufx.

Verified -level_09: GE_PCDUMP frames 46%/lower-band -> 91.7%/bbox
(0,20)-(639,459), correct letterbox + HUD placement. Reveals next layer:
room geometry (D85) and weapon model still don't draw (flat blue + HUD).
2026-08-28 20:38:07 -04:00
James Dansereau 7a72f37358 docs: RENDER CHAIN CLEAR — BUNKER1 renders with no fault (D101/D102 fixed); D75 model quality is next 2026-08-28 17:45:36 -04:00
James Dansereau 1d422d7dcc D102: give the 1P weapon Model + RW pool real storage (struct hand overlap)
gunfire.c punned the 1st-person weapon Model onto hand->field_B68 and its
RW-data pool onto hand->modeldatas (a fixed run at field_B68+0x20). N64
sizeof(struct Model) ~0xBC keeps modelInit()'s header writes (obj@0x08,
datas@0x10, scale, attachedto...) clear of the pool at +0x20. On x86-64
struct Model is 0xE8 (pointer fields widened 4->8): modelInit() writes
objinst->datas at offset 0x20 -- directly ONTO &hand->modeldatas -- so the
datas field and the pool base alias. The first rwdata record written by
modelInitRwData() then clobbers objinst->datas (bit 32 set), and
modelGetNodeRwData() returns 0x1_xxxxxxxx -> modelInitRwData faults.
objinst->obj (0x10) likewise lands on hand->mtxlist.

Add struct hand.weaponModel (inline struct Model) + weaponRwPool[192] under
#ifdef PORT and route gunfire.c through HAND_WEAPON_MODEL/HAND_WEAPON_RWPOOL
macros. On N64 hand->mtxlist aliased the punned model's render_pos, so set
weaponModel.render_pos = rwmtx explicitly on PORT. Same class as D53.2
(watch model) / D100 (player gait model). N64 build byte-identical.

Verified: -level_09 renders BUNKER1 with NO crash across 3 runs of 30-70 s
(previously: deterministic modelInitRwData fault at VI post ~601). Frames
render continuously (GE_PCDUMP: ~46% non-clear pixels, stable). The render
is partial -- bbox is the lower half of the screen -- consistent with the
still-open D75 3D-model rendering bugs, but the crash chain is clear.
2026-08-28 17:40:48 -04:00
James Dansereau b6cc42b42c D101: sub_GAME_7F06DB5C stashes ModelNode*/RenderPosView* through an s32
sub_GAME_7F06DB5C (the anim2-blend variant of modelBuildGroupMatrices) does
sp1C = (s32)arg2->Parent, then modelFindNodeMtx(arg1, (ModelNode *)sp1C, 0)
-- the 32-bit-pointer idiom its sibling modelBuildGroupMatrices() was
already cleaned up for. On x86-64 the (s32) cast drops the 0x140000000
module base of a static ModelNode, so modelFindNodeMtxIndex() dereferences
0x4012xxxx and faults. It also stashes &sp48[sp54] (a RenderPosView*) the
same way and calls g_ModelJointPositionedFunc with a bogus 3-arg cast.

Under #ifdef PORT, do the pointer work at full width and use the 2-arg
g_ModelJointPositionedFunc signature -- mirrors modelBuildGroupMatrices'
parentnode handling exactly. N64 path unchanged.

Verified: -level_09 no longer faults in modelFindNodeMtxIndex; advances to
a modelInitRwData crash (fixed by D102).
2026-08-28 17:40:48 -04:00
James Dansereau a24014dba6 docs: D100 partial (player.model inline struct); residual modelInitRwData garbage-index + modelFindNodeMtxIndex truncation 2026-08-28 17:17:20 -04:00
James Dansereau 0531aab76d D100: struct player.model is an inline Model, not a pointer (PC struct overlap)
Every use is `&g_CurrentPlayer->model` passed to a modelXXX(struct Model *)
function (animInit, modelTickAnim, modelSetAnimation, modelSetScale, ...) --
this field IS an inline `struct Model`. The decomp represents it as
`Model *model;` followed by ~45 `s32 field_59C..field_650` (roughly the N64
`sizeof(struct Model)`, ~0xB8 bytes). On x86-64 `struct Model` is ~0x2A8
bytes, so `animInit(&g_CurrentPlayer->model, ...)` wrote a 0x2A8-byte struct
into that 0xB8-byte hole, overrunning field_654 (the gait RW-data pool that
animInit is simultaneously pointed at), bondheadmatrices, and the
viewx/viewy/viewleft/viewtop fields. The clobbered `model->datas` came back
with bit 32 set -> modelInitRwData faulted at 0x1_70076xxx (a valid
player-block address | 0x100000000).

Fix (PORT only, N64 unchanged):
- struct player: `Model *model;` -> inline `struct Model model;`. sizeof grows
  ~0x2A0; D98's `mempAllocBytesInBank(sizeof(struct player) ...)` covers it.
- The gait RW-data pool: the fixed `&field_654` gap is only ~0x80 bytes and,
  with the bigger inline model, no longer even clears the struct. Add a
  dedicated `u32 gaitRwData[256]` at the end of struct player and point
  animInit there (initBondDATAdefaults.c).

Verified: the modelInitRwData bit-32 fault is gone (GE_D51 trace: the player
gait model's modelGetNodeRwData now returns clean pointers into gaitRwData).
-level_09 still reaches VI post ~601, then hits a separate deterministic
crash (3/3): modelFindNodeMtxIndex (model.c:375), FAULT 0x4012c9c0 -- a
truncated ModelNode* (low 32 of a 0x1_4012c9c0 module address), called from
sub_GAME_7F06DB5C (model.c:1479). Another D86/D92-class truncation in the
model node pipeline.
2026-08-28 17:15:38 -04:00
James Dansereau 16fc565d0b docs: D99 fixed (animflipfunc truncation); D100 modelInitRwData bad RW-data pointer is next 2026-08-28 17:00:58 -04:00
James Dansereau d356137f8c D99: animflipfunc s32 fn-pointer truncation in modelTickAnim
model->animflipfunc is an s32 Model field that stores a function pointer.
It is only ever set to bheadFlipAnimation (initBondDATAdefaults.c:198,
bondhead.c:430 -- both for g_CurrentPlayer->model). On x86-64 storing the
64-bit pointer truncates it; bheadFlipAnimation sits at module+0x10100 so
modelTickAnim's indirect call (model.c:3534) jumped to 0x00010100 and
faulted -- deterministically ~frame 5 on -level_09.

Same fix pattern as D92 (Model.unka0): modelSetAnimFlipFunction stores a
bool flag, and modelTickAnim calls bheadFlipAnimation() directly under
#ifdef PORT. N64 build unchanged.

Verified: the 0x00010100 fault is gone (2/2 runs); -level_09 now reaches
VI post ~601 (was ~421) before a separate, deterministic crash in
modelInitRwData (model.c:6298, MODELNODE_OPCODE_BSP case): modelGetNodeRwData
returns a bad pointer 0x170076434 (low 32 = 0x70076434, a valid player-
model DRAM addr, with bit 32 set) -- a D52/D86-class RW-data pointer-math
bug on the player's animated model.
2026-08-28 17:00:05 -04:00
James Dansereau 7d07bc2f3a docs: consolidate D95-D98 in PCPortResearch (prop room-list, damagetype, player struct); D99 modelTickAnim next 2026-08-28 16:55:19 -04:00
James Dansereau 62c4b2e4ff chore: gitignore .claude/ (Claude Code local state + agent worktrees) 2026-08-28 16:54:05 -04:00
James Dansereau 6d64c54909 docs: D98 fixed (player struct under-alloc scribbled master DL); modelTickAnim animflipfunc s32-truncation is next 2026-08-28 16:53:54 -04:00
James Dansereau 9ff1016fe1 D98: allocate the real PC sizeof(struct player), not the hardcoded N64 0x2A80
initBONDdataforPlayer allocates a fixed 0x2A80 bytes (US/JP; 0x2A70 EU) for
each player -- the N64 sizeof(struct player). On x86-64 the struct is far
larger (dozens of pointer fields widened 4->8 bytes), so every write past
~offset 0x2A08 runs off the block. The player block sits directly below
g_GfxBuffers[0] in MEMPOOL_STAGE, so g_CurrentPlayer->field_2A08 = ft4 in
bondviewRenderDebugBondView (reached every frame -- debug_render_raster
defaults to DEB_BOND_VIEW) wrote a small-negative-float angle onto the
master display list: the zbuf-clear gsDPSetRenderMode's w1 became 0xbd8efa35,
and fast3d aborted with 'Unknown GBI opcode 0xffffb9 at 0x70078330'.

Allocate (sizeof(struct player) + 0xF) & ~0xF under #ifdef PORT. N64 builds
unchanged.

Verified: -level_09 no longer hits the GBI-opcode fatal (was deterministic
at frame ~5). Renders further, then hits a separate corrupted-function-
pointer crash in modelTickAnim (model.c:3534) -- the D75-class animated-
model path, out of scope here.
2026-08-28 16:51:39 -04:00
James Dansereau 37454bc849 docs: D96 (prop room-list overflow) + D97 (damagetype clamp) fixed; 0xffffb9 master-DL slot corruption is next 2026-08-28 16:35:59 -04:00
James Dansereau 66d64d047c D97: clamp negative damagetype on PORT (US-build OOB read of g_DamageTypes)
bondviewPlayerTickDamageAndHealth computes damagetype = (s32)(health*8);
with health <= 0 (a lethal/near-lethal hit) that is negative and then
indexes g_DamageTypes[] below zero. N64 US absorbed the small OOB read;
on PC it segfaults (observed ~frame 5 in BUNKER1 when a guard shoots
Bond, fault ~0xff..401210e8). EU/JP already clamp negative to 0 right
here -- extend that clamp to PORT. N64 US build unchanged.
2026-08-28 16:35:25 -04:00
James Dansereau 1858b5d120 D96: PROPRECORD_STAN_ROOM_LEN 4 -> 8 on PC (prop room-list stack overflow)
chrpropUpdateRoomList and its helpers (sub_GAME_7F0B21B0 / sub_GAME_7F0BA2D4,
both max=7; s32 rooms[7] scratch) build prop room lists of up to 7 entries,
then write prop->rooms[0..count] + a 0xff/-1 terminator. PropRecord.rooms is
only PROPRECORD_STAN_ROOM_LEN (4) bytes, so a prop overlapping >=4 rooms
(BUNKER1 patrol guards routinely span 4: observed rooms=[1a 13 14 10],
[13 10 14 0f], ...) gets 4 real IDs and no terminator inside the array.

chraiGetPropRoomIds() then does `for (i=0; self->rooms[i] != 0xff; i++)
roomids[i] = self->rooms[i];` -- with no terminator it runs off the end and
overflows the caller's `s32 sp48[PROPRECORD_STAN_ROOM_LEN]` stack buffer in
chrpropsRenderPass, corrupting that frame. The garbage gdl that results drives
a GBI write to the top of emulated DRAM (0x70800000 / 0x90000000) around
frame 5 -- the "runaway GDL append".

Fix: size the field (and the matching sp48 local, same macro) for 7 rooms +
terminator, PC only -- N64 build byte-identical. Plus a defensive bound in
chraiGetPropRoomIds so a missing terminator can never walk past
roomids[LEN-1]. GE_D96=1 env probes left in chrprop.c.

Verified: the deterministic bgScissorCurrentPlayerView / 0x70800000 fault at
~frame 5 is gone (5 runs). BUNKER1 renders further before a separate,
less-severe failure -- a corrupt frame-GDL slot ("Unknown GBI opcode
0xffffb9", w0 sign-extended, garbage w1) around frame 5-6, and a rarer
truncated-pointer segv in bondviewPlayerTickDamageAndHealth (bondview2.c:6174).
2026-08-28 16:31:12 -04:00
James Dansereau fea435acf5 docs: D95 mempool-ceiling fix (renders ~5 frames); runaway GDL append at frame 5 (prop render) is next 2026-08-28 16:12:36 -04:00
James Dansereau d13105147d D95: raise the PC mempool ceiling to reclaim ~4 MB of unused DRAM
The N64-fidelity mempool end (0x702F4400, tlbmanageGetTlbAllocatedBlock)
leaves ~5 MB of live, mapped, unused DRAM below the 8 MB top (only
animations_frame_buffer at 0x707FFD30 lives up there). PC structs are
larger everywhere, so the per-level -m* budget no longer fits and
MEMPOOL_STAGE OOMs (zbufAllocate spins in mempAllocBytesInBank). Raise
the ceiling to 0x70700000; the ~4 MB gain goes to MEMPOOL_STAGE.

With this + the reapplied 2x g_GfxBuffers (128f7644), -level_09 renders
~5 frames (previously: OOM hang, or non-deterministic overrun within
seconds). It then still faults in bgScissorCurrentPlayerView writing at
0x70800000 -- but deterministically at ~frame 5 and regardless of -mgfx
size (tested up to 1.6 MB/half), so that is a RUNAWAY GDL append in the
prop-render path (chrpropsRenderPass/chrpropRender), not a buffer-size
problem. Next blocker.
2026-08-28 16:11:49 -04:00
James Dansereau 128f7644f6 Reapply "D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)"
This reverts commit 9f53c260ee.
2026-08-28 16:09:30 -04:00
James Dansereau 4eedcd8c24 docs: D94 fixed (chraction ptr truncation); D95 open (PC -mgfx buffer half-capacity, naive fix OOMs stage pool) 2026-08-28 16:08:05 -04:00
James Dansereau 9f53c260ee Revert "D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)"
This reverts commit 6229d20a6d.
2026-08-28 16:06:45 -04:00
James Dansereau f96ad3ee78 D85: env-gated GE_D85DUMP probe -- dump post-texLoadFromGdl room GDL stream 2026-08-28 16:04:37 -04:00
James Dansereau 6229d20a6d D95: double the -mgfx master-DL buffer on PC (16-byte Gfx vs N64 8-byte)
g_GfxSizesByPlayerCount / -mgfx is a BYTE budget sized for N64 8-byte Gfx
slots. On x86-64 a Gfx is 16 bytes, so the master display list needs 2x
the bytes for the same command count. Every render fn appends with a bare
'gdl++' and no bounds check, so once BUNKER1 emits real room geometry the
list overruns g_GfxBuffers[1]/[2], runs off the stage mempool, and faults
writing a GBI command at the end of the 8 MB emulated DRAM (0x70800000) --
non-deterministically, and corrupting room GDL blobs on the way (the
'unknown GBI opcode' / seg-9 0x90000000 faults were downstream of this).

Scale the gfx allocation by sizeof(Gfx)/8 under #ifdef PORT. Vtx/Mtx are
16/64 bytes on both targets so g_VtxBuffers is unchanged.

Verified: -level_09 runs 90 s+ (5000+ VI posts) with no crash where it
previously faulted within seconds. (Separate open issue: the kernel
heartbeat reports frames=0 -- render-loop completion needs a look.)
2026-08-28 16:04:37 -04:00
James Dansereau 83b7287f07 D94: truncating (s32) pointer cast in chrlvInitActAttack anim-table indexing
arg1[anim_index]->table is 'weapon_firing_animation_table (*)[]'; the
'(s32)table + (s32)(idx*sizeof(...))' form truncates the 64-bit pointer
to 32 bits (then zero-extends on the cast back), dropping the module
load-base high word (0x1_40000000 -> 0x4012xxxx) so panim_float->anim.anim
faults ~0x4012xxxx once a guard starts an attack in-level. It is plain
array indexing (the code's own comment says so) -- do it as such under
#ifdef PORT, both occurrences. Behaviour-identical on N64.

Verified: -level_09 no longer faults in chrlvInitActAttack across 8+ runs.
2026-08-28 16:04:36 -04:00
James Dansereau 89cbf727a6 docs: D85 texture wall CLEARED (BUNKER1 renders frames); non-deterministic crash cluster is next
Also documents the data/ junction-deletion + full sidecar-regen recovery.
2026-08-28 15:52:35 -04:00
James Dansereau b6f9e5113b docs: D85 texture-pool fixed (real texpool storage); frame-GDL overrun is the new wall 2026-08-28 13:27:29 -04:00
James Dansereau c18c8b8366 D85: give the stage texture pool (ptr_texture_alloc_start) real storage
The decomp declares ptr_texture_alloc_start as `struct texpool *` but no
code ever dereferences it as a pointer -- every use takes its address and
treats the storage itself as a `struct texpool` (texInitPool writes all
four members through &ptr_texture_alloc_start; texLoad / texFindInPool
read them back). On N64 the four members are 4 bytes each (16 total) and
overlap the following split-out symbols (ptr_texture_alloc_end etc.); the
layout happens to work. On x86-64 `struct texpool` is 32 bytes (four
widened pointers), so texInitPool smashes 24 bytes past the 8-byte
pointer global and, worse, reads of ->leftpos / ->rightpos (offsets 16
and 24) come back as unrelated BSS -- the stage texture pool then looks
permanently exhausted (texFreeBytesInBuffer() < 0) the very first time
texLoad() runs on the -level_09 path. Every room texLoad bails at its
out-of-memory guard without adding to the pool, texFindInPool() returns
NULL for every room texnum, and gfx_pc aborts with 'Bad size for RGBA
texture in tile 0: 00'.

Fix: under PORT, define ptr_texture_alloc_start as a real `struct
texpool` (not a pointer). All call sites already do
`&ptr_texture_alloc_start`, so they now get a correctly-typed
`struct texpool *` and the N64 build is untouched.

Verified: -level_09 stage texture pool now fills normally (no premature
OOM), ~630 room textures resolve, and the 'Bad size for RGBA texture'
FATAL is gone. Rendering now advances to a separate issue -- gfx_sp_matrix
dereferencing an unrelocated N64 segment address (0x90000000) from a room
GDL gsSPMatrix, the next D85 layer.

Also renamed the D85 texLoad pool-full probe (GE_D85TEX) from 'OOM' since
with this fix it now only fires on a genuinely full pool.
2026-08-28 13:26:23 -04:00
James Dansereau 419a50a677 docs: D85 stream-decode fixed (GDL widen+bswap); texture-pool load is the new wall 2026-08-28 13:14:57 -04:00
James Dansereau f8cb0ba406 D85: GE_D85TEX probe in texLoadFromGdl (room texture triage)
env-gated (zero cost when unset), left in place: logs each G_NOOP
texture marker's decoded texnum / type / whether texFindInPool resolved
it, and passes-through G_SETTIMG/G_SETTILE-class opcodes. Used to
establish that room GDL texnums decode correctly post-widen but every
lookup returns NULL -- the room texture bank isn't populated on the
-level_09 path.
2026-08-28 13:13:55 -04:00
James Dansereau 4958ca1c7a D85: widen + byte-swap raw N64 room GDL / Vtx blobs at load time
The per-room primary/secondary display-list binaries and the point-index
(Vtx) binary are copied verbatim by the offline bg converter (D80 left
the DL/point-index tail unconverted). Unlike model GDLs -- which the
pcmodels sidecar (D43/D50) pre-widens to 16-byte Gfx_le slots and
byte-swaps -- nothing converts the room blobs, so every PC consumer
(texCopyGdls, texLoadFromGdl, bgApplyDynamicCCRMLUT, bgBuildRoomVtxBounds)
strode 16-byte slots over 8-byte big-endian N64 data and decoded a
garbage GBI stream (cmd=00/01/02/52...). That is the D85 "room GDL
decodes to garbage" blocker.

Fix (runtime, not a sidecar -- the blob is RZ-compressed inside the bg
.seg and delta-sized, so an offline widen would have to rewrite the
whole compressed header offset table):

- bgWidenRoomGdl(): in-place, back-to-front 8->16 widen of the freshly
  decompressed blob, bswap32 on each word. w0's top byte becomes the
  opcode, matching port/shim/PR/gbi.h's Gfx_le. Called in
  bgLoadRoomPrimaryGdl / bgLoadRoomSecondaryGdl between bgDecompress and
  texCopyGdls; the widened (2x) size is what flows into texCopyGdls,
  texLoadFromGdl and usize_*_DL_binary. The room's memaAlloc block is
  sized from memaGetLongestFree() on first load and cached from the
  post-widen `used`, so the 2x expansion needs no budget-formula change
  (every N64 offset in the scratch math simply doubles).

- bgSwapRoomVtx(): bswap16 the 6 leading u16 fields of each Vtx
  (ob[3], flag, tc[2]); the 4 cn[] colour bytes are left alone. Vtx is
  16 bytes on both targets so no widening.

- bgLoadRoomSecondaryGdl also had `texLoadFromGdl((Gfx *)scratch,
  (Gfx *)expanded_size, ...)` -- a stray pointer cast of the size arg;
  fixed under #ifdef PORT, N64 path unchanged.

Verified: with this in, `-level_09` room GDLs decode to real GBI
(GE_D69BB dump: E7 RDPPIPESYNC / BA SETOTHERMODE_H / B9 SETOTHERMODE_L /
FC SETCOMBINE / BB TEXTURE / B7 SETGEOMETRYMODE preamble) and the
G_NOOP texture markers decode to sane texnums. Two downstream issues
remain (not stream-decode): room textures are never found in the pool
(texFindInPool returns NULL for every room texnum -> "Bad size for RGBA
texture"), and a frame-GDL buffer overrun in chrpropsRenderPass.
2026-08-28 13:13:55 -04:00
James Dansereau e61bc1fae1 docs: D93 resolved (null-room NULL guards); D85 root cause confirmed (8B vs 16B Gfx) 2026-08-28 13:05:16 -04:00
James Dansereau fe9c13af66 D85: NULL-guard the null-room (room 0) GDL patch/bounds walks
Room 0 has no geometry (csize_*_DL_binary == 0), so bgLoadRoomModelData
never assigns ptr_expanded_mapping_info and it stays NULL. Two consumers
then walk it unconditionally:

- bgApplyDynamicCCRMLUT: called with start=NULL, end=NULL; the end==NULL
  sentinel-scan branch dereferences address 0.
- bgBuildRoomVtxBounds: 'while (gdl[i].dma.cmd != G_ENDDL)' on gdl=NULL.

On N64 address 0 is readable RDRAM so both walks wander harmlessly; on PC
page 0 is unmapped. Add narrow #ifdef PORT NULL guards (same safety-net
pattern as the existing D85 bgBuildRoomVtxBounds vtx-bounds check).

These sat in front of the real D85 blocker: -level_09 now reaches the
documented 'Bad size for RGBA texture' assert (room GDL still decodes to
a garbage GBI stream -- 8-byte N64 Gfx vs 16-byte PC Gfx).
2026-08-28 13:03:20 -04:00
James Dansereau cfef983c66 docs: D90/D91/D92 resolved; D85 (room GDL) is now the render blocker 2026-08-28 12:39:18 -04:00
James Dansereau d2177db3a5 D92: two truncated pointers on the chr/AI spawn path
(a) chrAllocate's 5th param was declared s32 but callers pass
ailistFindById()'s AIRecord* -- the 64-bit pointer was truncated before
reaching init_GUARDdata_with_set_values, so chr->ailist held a bogus
address and ai() faulted dereferencing (AiListp + Offset)->cmd on the
first AI tick. Param widened to AIListRecord* under PORT.

(b) Model.unka0 is a 32-bit field that on N64 holds a function pointer
(always sub_GAME_7F01FC10). chr.c stored (s32)sub_GAME_7F01FC10 into it
and model.c subcalcpos called it back -- truncated jump target on PC.
Widening the field would shift the rest of Model, so under PORT the
setter stores a flag and subcalcpos calls sub_GAME_7F01FC10 directly.

BUNKER1 now loads and ticks through to the first render, where it hits
D85 (room GDL/texture decode: 'Bad size for RGBA texture').
2026-08-28 12:38:11 -04:00
James Dansereau b2a77d53ec D91: bg portal-descend truncated the D_800442FC element address
sub_GAME_7F0B7F84 (both variants) does `i = (s32) &D_800442FC[portalnum]`
then `*((u8 *) i) = depth` -- the (s32) cast drops the top 32 bits of
the array address on PC, so the byte store lands on a bogus low pointer
and faults during portal occlusion culling. Under PORT keep `i` as a
plain value (only used as an `if (i);` no-op afterwards) and write the
byte to D_800442FC[portalnum] directly.
2026-08-28 12:38:02 -04:00
James Dansereau 5577f54119 D90: stanTileDistanceRelated zero-fill overran the caller's stack
The N64 "HACK" loop in stanTileDistanceRelated zero-fills 20 s32 words
(80 bytes) from &record -- far past sizeof(StandTileLocusCallbackRecord)
(16B). On N64 the overrun hit adjacent stack scratch harmlessly; on PC
the different frame layout (plus pointer-widened locals) put the
caller's live current-tile-ptr local in the blast radius, so the stan
locus walk started from NULL and faulted in stanIsSpecialBit1Set on the
first player collision tick. Clear exactly the 4 record fields under
PORT (matches sub_GAME_7F0B21B0's field-by-field init).

Adds GE_D90 probes (pad->stan resolution, spawn pad selection, NULL
current_tile_ptr canary), env-gated, zero cost when unset.
2026-08-28 12:37:56 -04:00
James Dansereau 1df8e8e29f docs: D88.5/D88.6/D89 resolved, D90 (player stan NULL) opened
Session M-2: -level_09 boot wired, stan tile-name + intro-lang converter
fixes, two load-path crash fixes. BUNKER1 now renders ~2100 frames on
both direct-boot and attract before D90.
2026-08-28 09:59:35 -04:00
James Dansereau c43a4599cf D89: fix two crashes on the path from stage-load to first frame
init_path_table_links: validationGroupCursors[-3] is a decomp artifact
(constant negative index into a 1-element stack array). GCC proves it
out of bounds and compiles it to a trap -- SIGILL on PC. Point the name
3 elements into a real 4-element backing buffer under PORT so the
identical [-3] expressions land in bounds; N64 build keeps the plain
array and is unchanged.

sub_GAME_7F0B0914 (walkTilesBetweenPoints): callers such as
domakedefaultobj pass &pad->stan, which is legitimately NULL when a
pad's stan name did not resolve. On N64 the walk then reads ~0 for
pointCount and returns TRUE via the crossings==0 early-out; on PC the
near-NULL read segfaults. Guard *tileStack == NULL -> return TRUE.
2026-08-28 09:58:29 -04:00
James Dansereau 2f3bc74417 D88.6: intro CAMERA lang1c is a u16 pair, not an s32
SetupIntroCamera.lang1c is union { u16 lang_index[2]; u32 lang_ptr; }
and bondview_r.c reads lang1c.lang_index[1]. The intro converter
bswap32'd the whole word, which swaps element 0 and element 1 -- the
consumer then read the wrong language slot id, indexed g_LangBanks out
of range and crashed in langGet on a NULL bank. Byte-swap each u16 of
lang1c in place instead; lang20 stays a genuine s32 bswap32. Requires
d88_emit.py --regen.
2026-08-28 09:58:22 -04:00
James Dansereau 5dbe8a48c2 D88.5: byte-swap the stan tile id-hi half in the converter
stanMatchTileName reads a tile's packed name id through a StandTilePoint
alias: (u16)tile->x == stanIdHi and *((u8*)&tile->y) == stanIdLo. D78
left the id/room word as a verbatim byte array ("id is provably dead"),
which missed this aliased scalar read -- on little-endian PC the u16
load of the big-endian id-hi bytes came back byte-swapped, so every
name lookup during pad setup missed (0/276). Swap bytes 0-1 of the
id/room word in d69_emit's stan converter; idLo (byte 2) and room
(byte 3) stay put. Verified: 273/273 matches, pads now get real stan
tiles. Requires sidecar regen (d69_emit.py + d88_emit.py --regen).
2026-08-28 09:58:14 -04:00
James Dansereau ac8319700b D69/D50: patch bg/stan + model sidecar tables in obInit()
pccgPatchTable()/pcmodelsPatchTable() were only called lazily from
load_object_fill_header (first model load). A direct -level_XX boot
loads a stage before any model loads, so load_bg_file read raw
big-endian ROM and crashed on the unconverted .seg header. Move the
one-shot (idempotent) patch calls to the end of obInit(), which runs
before any stage load on every path.
2026-08-28 09:58:08 -04:00
James Dansereau 03dcda7bc3 port: feed host argv into the game token string
osPiReadIo was stubbed to return 0, so token.c's cartridge-token read
always produced an empty string and N64 debug switches (-level_09,
-hardN, -m*) were silently ignored -- the game could only ever reach a
level via attract-mode demo playback. Synthesise the token string from
argv[1..] (sysGetTokenString) and serve its bytes from the 0xFFB000
range in osPiReadIo, so -level_09 now boots straight into BUNKER1.

Empty when no args are passed, so no-arg (attract) runs are unchanged.
2026-08-28 09:58:02 -04:00
James Dansereau 5fe167d8fd D88.4: mark resolved in PCPortResearch.md §F + HANDOFF.md
-level_09 loads BUNKER1's full stage setup (pads + propDefs) and renders
1000+ frames with zero FATAL/EXCEPTION. Next milestone: D75/D85 render bugs.
2026-08-28 09:17:42 -04:00
James Dansereau 2f8182307b D88.4: sizepropdef() PORT stride matches the grown PC propDef records
#ifdef PORT branch returns PROPDEF_PC_BYTES/4 for each type so the in-place
prop-setup walk (prop.c, loadobjectmodel.c, objective_status.c) advances by
the same stride the converter emits. Only OBJ_COPY_ITEM's N64 literal was
actually wrong (1, real 3); the rest match the N64 counts plus pointer
growth. Non-BUNKER1 types (VEHICHLE/AIRCRAFT/TANK/AMMO) still need probe
confirmation before those levels load.
2026-08-28 09:16:13 -04:00
James Dansereau 695f42d383 D88.4: emit converted propDefs into the Usetup*Z sidecar
d88_emit.py runs d88_propdefs.convert_stream over the propDefs region and
feeds its growth into the cumulative delta so intro + every later table
shifts correctly.
2026-08-28 09:16:13 -04:00
James Dansereau f2281ed661 D88.4: propDefs record format spec + N64->PC converter (d88_propdefs.py)
The propDefs stream is a flat s32[]; each record's serialized N64 word count
is fixed across all 21 levels (verified: the getools C sources tile every
propDefs region byte-for-byte against the ROM). Every pointer member is 0 in
the file, so records containing pointers grow 4->8B per pointer on PC.

d88_propdefs.py rewrites each record to its native PC struct layout: header
word (u16+u8+u8) and _mkword half-pairs swapped independently, scalars
bswap32'd, pointer slots widened to 8 zero bytes, runtime areas zero-filled.
d88_layoutprobe.c (expanded) is the compiler-verified sizeof/offsetof source
for PROPDEF_PC_BYTES. Converts + walk-verifies all 21 levels clean.
2026-08-28 09:16:05 -04:00
James Dansereau f333a713fd D88.4: d88_emit.py --regen flag + PC layout probe for propDef records
--regen drops all Usetup*Z rows (manifest + bytes) and rebuilds the sidecar
from the surviving d69 rows before re-converting, for clean converter
iteration. d88_layoutprobe.c prints sizeof/offsetof for every propDef record
struct against the real port include chain.
2026-08-28 09:04:46 -04:00
James Dansereau c09bf37c66 D63: gate dram-branch G_DL trace behind GE_D63 (was flooding -level_09) 2026-08-28 09:04:46 -04:00
James Dansereau fbfa2f8231 D88.4: add propDefs region scanner (analysis helper, WIP)
Read-only tool to enumerate PROPDEF_* record types per Usetup*Z file.
Walk currently drifts on the sizeof()-derived record types (DOOR/GUARD/
PROP/etc) because their exact N64 word sizes aren't yet confirmed -- that
confirmation is the core remaining D88.4 spec task.
2026-08-28 08:33:31 -04:00
James Dansereau 1e1e322240 D88.4: expand blocker scope note (propDef records grow on PC, not just bswap) 2026-08-28 08:31:50 -04:00
James Dansereau 48aba25c62 D88: verify D88.1-3 via -level_09; document D88.4 blocker + D75 render bugs
- prop.c/stan.c: GE_D88 env-gated diagnostic probes (proplvreset2 pad
  walk, stanMatchTileName tile walk). Zero cost when unset.
- Verified: proplvreset2 now walks the full pads table correctly
  (real plink strings, sane BUNKER1 coords); prop.c:1306 crash gone.
- D88.4 (new blocker): propDefs stream left un-byteswapped by the
  converter -> setupDoor/modelLoad crash on garbage door->obj.
- D75: expanded with confirmed symptoms (Nintendo logo mispositioned,
  gun-barrel Bond model missing, cast-roll character models absent).
2026-08-28 08:30:56 -04:00
James Dansereau 4e1b891fc6 D88.1/D88.3: SetupIntroCamera narrow-pointer ABI + pccg wiring
- bondtypes.h: SetupIntroCamera lang1c/lang20/prev kept u32 under
  #ifdef PORT so sizeof stays 40 and the fixed-stride intro-record walk
  in bondview_r.c/bondview2.c still matches 40-byte file records. Fields
  are write-before-read (bondview_r.c:276-300 writes all three before any
  read via langGet()/list-link), verified. Same class as D79/D53.1.
- bondview2.c/bondview_r.c: cast (char *)(uintptr_t) at each use site.
- pccg.c: PCCG_MAX_FILES 128 -> 256 to also hold the Usetup*Z rows.
2026-08-28 08:30:56 -04:00
James Dansereau cca7d0bfad D88.2: Usetup*Z stage-setup offline converter (tools_pc/d88_emit.py)
Converts the 21 per-level Usetup*Z stage-setup files to PC layout,
appending rows to the same pccg.bin/manifest sidecar as d69_emit.py.
Delta-relocates the 8 pointer-widened sub-tables (header 40->80B,
waypoint/waygroup/PathRecord/AIListRecord/PadRecord/BoundPadRecord/pname)
and byte-swaps the s32 ID/offset fields. propDefs left as an opaque
relocated passthrough for now (D88.4).
2026-08-28 08:30:56 -04:00
James DansereauandClaude Sonnet 5 0bcf5f1777 D86+D87: player gait-model pointer truncation + attract-mode demo endian fix
D86 (resolved): modelInitRwData crashed on a bad ModelNode pointer during
room-triggered gameplay init. Root cause: initplayergaitobject.c did
`player_gait_object_header.RootNode = (int)&player_gait_hdr;` -- a
same-width pointer<->int round trip that's a no-op on N64 but truncates a
real 64-bit pointer on PC, dropping the executable's load-base high bits.
Fixed under #ifdef PORT by assigning the real pointer directly. Only
exercised once real gameplay starts (past the intro), which is why it was
never hit before D69 let the game reach stage load.

D87 (resolved): once D86 stopped blocking progress, an idle (no-input) run
reaches the front end's genuine attract-mode demo playback and crashes in
ramrom_replay_handler. Root cause: ramromfilestructure is a real
ROM-compiled asset (big-endian) loaded via romCopyAligned's raw byte copy
(D66) with no byteswap, so every multi-byte field reads scrambled and
drives wild pointer arithmetic in the block-buffer consumers. Fixed with a
#ifdef PORT ramromFixupEndian() called once after the load (D54 cseq-fixup
pattern).

Both root-caused with new env-gated TEMP probes (GE_D86, GE_D87), left in
place per the existing convention. D88 (per-level "Usetup*Z" stage-setup
file: zero PC porting done, struct-width + endian bugs stacked) is the new
next blocker, root-caused but intentionally not patched inline -- it's
D69-scale format-conversion work, logged as an open finding instead.

Also documents a fast, deterministic BUNKER1 repro (-level_09 command-line
arg, bypasses the front end/attract-mode entirely) and a correction to the
gdb guidance: attach mode (vs. launch-only) works well for watching a
global's write on an already-running process.

Docs: docs/PCPortResearch.md §F (D86/D87 resolved, D88 new), docs/HANDOFF.md
(session K), AGENTS.md summary line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
2026-08-27 21:47:04 -04:00
James DansereauandClaude Sonnet 5 1d4e912e83 D69.4: verification fixes (D83/D84) + crash safety net (D85) + docs
Verification of the D69 sidecars against a live run surfaced two more
narrow ABI layout bugs, both the same MIPS-BE/x86-LE class as D78/D79:

- D83: StandTileHeaderMid/StandTileHeaderTail bitfield packing. N64/MIPS
  packs the first-declared bitfield into the high bits; x86 GCC packs it
  into the low bits. tile->tail.hdrTail.pointCount (read pervasively for
  tile-size navigation) silently read the wrong nibble on PC, producing
  an out-of-range list_of_tilesizes index and an infinite loop in
  stanBuildRoomData. Fixed under #ifdef PORT by declaring both structs'
  fields in reverse order (verified via probe: byte-identical numeric
  result to N64).

- D84: bgLoadRoomVtxData/bgLoadRoomPrimaryGdl/bgLoadRoomSecondaryGdl each
  hand-roll the BG_SEG_TO_PTR segment fold instead of calling the macro,
  and did it as real 64-bit pointer arithmetic -- the "+0xf1000000"
  doesn't wrap at 32 bits the way the macro's (u32) cast does, so the
  computed file offset came out ~4 GiB too large. Fixed under #ifdef PORT
  by doing the fold as plain u32 math at all three sites, matching
  BG_SEG_TO_PTR exactly.

With D83+D84, load_bg_file and stanBuildRoomData complete without fault
and the game proceeds into room streaming, where it hit a further crash
in bgBuildRoomVtxBounds: the room primary/secondary DL binaries decompress
correctly but texLoadFromGdl (the already-working model-GDL runtime
converter) doesn't produce valid GBI opcodes from their content (D85,
not yet root-caused -- likely a BG-specific GDL marker convention).
Added a PORT-only bounds check before every vtx[] dereference so a
garbage command stream can't segfault the process (produces an empty
bounding box for that batch instead) -- a crash-safety net, not a
geometry fix.

The original D69 bug (load_bg_file faulting instantly on stage load) is
now fully resolved and verified byte-for-byte against BUNKER1's stan
tiles. Two new, separate issues surfaced during verification remain open:
D85 (room geometry decodes wrong) and D86 (a new crash in modelInitRwData
during room-triggered model instantiation, likely unrelated to bg/stan --
see docs/HANDOFF.md). Debug probes GE_D69STAN/GE_D69BB added (stan.c/
bg.c, env-gated, zero cost when unset) and kept for the next session.

Docs: PCPortResearch.md D83-D86, HANDOFF.md and AGENTS.md status rewritten
to reflect current state.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
2026-08-27 21:02:31 -04:00
James DansereauandClaude Sonnet 5 81d5614fbd D69.3: bg/stan sidecar port-layer wiring (port/src/pccg.c)
Serves the offline-converted sidecars from tools_pc/d69_emit.py, cloned
from port/src/pcmodels.c (D50 Plan-B pattern): pccgReserveSize/
pccgLoadSidecars/pccgPatchTable/pccgTotalSize/pccgSidecarBase.

Wiring: romdata.c extends the CART_BASE VirtualAlloc reservation to
include pccgReserveSize() right after the pcmodels extension and loads the
sidecar image there; romdataCartAddrValid's bounds check and libultra.c's
D60 DMA-source diagnostic both extended to cover the pccg byte range.
One-shot pccgPatchTable() call added next to pcmodelsPatchTable() in
load_object_fill_header (objecthandler_2.c) -- reused purely as a
"definitely after obInit()" hook, unrelated to model loading.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
2026-08-27 21:02:12 -04:00
James DansereauandClaude Sonnet 5 2acb7f6a42 D69.2: bg/stan offline converter (tools_pc/d69_emit.py)
Implements the D80-D81 format spec: size-preserving bswap of bg .seg
header/room/envdata tables, growth+relocation of the portal_data_entry
table (8B N64 -> 16B PC, offset_portal stays a real dereferenced pointer)
and its point-data blob (relocated by portal_delta, content size
unchanged), and the Tbg_*_stanZ prefix room-offset array (4B N64 -> 8B PC
slots, matching StanPrefixRecord's PC-compiled pointer alignment) plus
per-tile bswap of mid/tail halves and point coordinates. Round-trip
validated against BUNKER1 (bg/bg_sev_all_p.seg, Tbg_sev_all_p_stanZ):
header offsets, MaxNumRooms walk, portal targets/connectedRoom bytes, and
all 1066 stan tiles match the N64 source exactly.

Output: data/pccg-<region>/pccg.bin + manifest.csv (gitignored, same shape
as pcmodels.bin/D43). Regenerate: python tools_pc/d69_emit.py ntsc-final.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
2026-08-27 21:02:02 -04:00
James DansereauandClaude Sonnet 5 12e957347c D69.1: bg/stan format spec + StandTile/bg_room_data ABI layout fixes (D78-D82)
Docs-of-record for the D69 stage-load milestone (docs/PCPortResearch.md
D78-D82): decoded bg/*.seg (header/room/envdata/portal tables) and
Tbg_*_stanZ (prefix offset-array + StandTile records) formats, plus two
struct-layout ABI fixes required before any byte-swap conversion can work:

- D78: StandTile's `u32 id:24` + non-bitfield `u8 room` packs into one
  4-byte word on N64/MIPS-GCC (8-byte tile header) but never shares
  storage on x86 GCC (10-byte header), silently misaligning every
  mid/tail/points field read on PC regardless of endianness. `id` is
  provably dead code (no reads/writes anywhere) so it's widened to a
  3-byte array under #ifdef PORT, restoring the exact N64 stride.

- D79: bg_room_data's 3 `void *` offset fields are never dereferenced
  (grep-verified, always used as plain u32 arithmetic) but silently grow
  the 24-byte N64 room record to 40 bytes on x86-64. Declared `u32` under
  #ifdef PORT instead -- size-preserving, no behavior change.

Both are narrow, individually-documented #ifdef PORT layout exceptions
per AGENTS.md non-negotiable #2 (same class as D53.1/D53.2/D66).

Adds docs/BRIEF-D69-stage-load.md (milestone brief) for reference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C3xib6jjMV8o2iE4JfixAo
2026-08-27 20:22:08 -04:00
James Dansereau 0010270f1b Repo cleanup: reference footage to docs/reference/, ignore local artifacts
- Move the two N64 reference screenshots out of the repo root into
  docs/reference/ (n64-footage-2026-08-24.jpg, n64-footage-gun-barrel.png);
  HANDOFF.md now points there.
- Gitignore screenshots/ (session captures) and tools_pc/mupen64/ (local
  Mupen64 Plus checkout used for N64 reference footage; 8 MB of binaries).
- Commit tools_pc/mtxtest.c, the standalone fast3d matrix-convention
  harness that HANDOFF/D72.3 docs reference as kept-for-reference.
- Refresh PORTING.md: status was still 'scaffolding / building once
  implementation lands'; now reflects Phase 2 in progress and points at
  docs/HANDOFF.md for current session state.
2026-08-27 19:27:05 -04:00
James Dansereau d3b3fe53ce Docs: track open issues D75-D77 (3D render, 2D partial draw, no audio)
Carry over the logo-pixel session's handoff state: D75-D77 recorded as
OPEN in PCPortResearch.md §F with candidate causes + verification steps,
prioritization guidance (D69 stays primary; render before audio unless a
shared root cause is found - Q1), and matching AGENTS.md phase-status line.
HANDOFF also drops the stale scratch-file list from Task 3 and points at
the relocated N64 reference screenshots.
2026-08-27 19:26:57 -04:00
James Dansereau 978306d14f D73+D74: intro logo fixed (sinf/cosf endianness + texture import/sub-tile wrap)
D73 (root cause of old D72.3): GE's sinf/cosf build their double
constants through the du bit-union with big-endian word pairs {hi,lo};
on LE PC the .d read has the words swapped (rpi.d ~ 2^733), range
reduction overflows, and FTOFIX32 saturates to -32768 for every
sin/cos entry of guMtxF2L — so the Rareware logo's rotation matrix was
garbage and all its triangles projected off-screen. Fix: DVAL() macro
in src/libultra/gu/guint.h (PORT-only; #else branch is the original
expression) re-packing hi/lo into the correct LE double; sinf.c/cosf.c
.d reads replaced. N64 build untouched.

D74 (texture side, three port-layer bugs in fast3d):
- import_texture's fallback condition (rdp.tex_lod && tile >=
  first+detail) overwrote valid gDPLoadBlock tmem data with
  line*tile.height: letter mip chains 2744B -> 2048B (mips 1-5 dropped),
  D_02005FF0 32x32 -> 32x3 (stale tile.height from gsDPSetTileSize).
  Now falls back only when the tmem slot was never written.
- TextureCacheKey gains size_bytes so a truncated first import cannot
  poison later full imports of the same address.
- VBO UV path now wraps by TILE size for WRAP sub-tiles (N64 semantics);
  the logo's 20x3 tile at offset (11.5,29) was sampling row 0 instead
  of rows 29-31. Per-vertex pre-wrap; known limits: half-texel edge
  bleed, mirror sub-tiles unhandled.

Verified: clean runs reach frame ~2100 then hit the D69 load_bg_file
fault (unchanged — still the milestone blocker); PPM frame ~555 shows
the four gold RARE letters on the dark-blue plate (final pixel-perfect
check vs N64 footage left as a low-priority task). All env-gated probes
from this session (GE_DBGUV/DBGTRI/DBGTALL/DBGMAT/D74IMP/D74DUMP/
GE_DBGLOAD) stripped; previously committed TEMP D63 blocks + GE_D71LOG
remain on the HANDOFF strip list.

Docs: PCPortResearch.md §F D73/D74 entries, §G/§H status updated;
HANDOFF.md rewritten for Session I (D69 primary); AGENTS.md phase line.
2026-08-24 22:32:27 -04:00
James Dansereau 5f4e90ddfc Docs: sync all state trackers to D70-D72 (two live threads: D72.3 logo pixels, D69 stage load)
- PCPortResearch.md: G status + H handoff now list D70-D72, the open D72.3
  bug with its next diagnostic step, and the env-gated visual-debug tooling;
  stale 'current frontier (post-D31)' note in the plan marked done
- AGENTS.md: phase status carries the D70-D72 summary; D69 stays the
  milestone blocker
- HANDOFF.md: title reflects both threads
2026-08-24 19:16:23 -04:00
James Dansereau 8ed411d3f1 D70-D72: PPM capture tool, C-array texture byte-order fix, tc[] UV restore
- D70: GE_PCDUMP env-gated PPM frame dump (video.c + gfx_opengl.cpp); ppm/ gitignored
- D71: per-source bswap of C-array texture sources in import_texture (the four
  rarewarelogo.c RGBA16 images are LE u32 arrays on PC -> pink/green logo)
- D72: removed PD-inherited normal/lookat UV override; lookat_enabled defaults
  false (GE always uses authored tc[] UVs; N64 boots with RSP mem zeroed)
- D72.3 OPEN: logo triangles still project off-screen; a flat dark-blue
  checkerboard triangle is rasterized instead - identity pending
  (GE_DBGUV/GE_DBGTALL env-gated traces added)
- Docs: PCPortResearch.md F/D70-D72, HANDOFF refreshed, D71 log gated
2026-08-24 19:12:40 -04:00
James Dansereau 8f631462f3 PC port: phase 2 — D59-D68 milestone: full intro renders; blocker is D69 (BG-file BE headers)
D59/D64: the 'external GL DLL' crash was an unbounded blood-RLE write
(sentinel die_blood_image_end lost its N64 section adjacency on PC)
clobbering the gun-barrel sub-DL at ptr_logo_and_walletbond_DL+0x200.
D60: osPiStartDma DMA-target validation (VirtualQuery-based) in libultra.c.
D61/D63: TEMP env-gated ROM-read / VTX-pool / menu probes (strip list in
HANDOFF Task 3). D62: OSMesgQueue/OSScMsg PC layout verification.
D65/D65b: enum HEADS/BODIES signed sentinels under PORT (PC GCC picks
unsigned underlying types; N64 semantics were signed).
D66: romCopyAligned + ramrom replay 64-bit pointer width (s32 truncation
of .bss targets > 4 GiB). D67: struct image_entry N64 layout
reconstruction (dataoffset:24 first, all-u32 bitfields, sizeof==8).
D68: Globalimagetable BE->LE fixup (port/src/gimgfixup.c): bswap the
IMAGESEG-marked Gfx w1 words in the 17 ROM-copied DLs + sImageTableEntry.
index in the 32 tables after texReset's romCopy; marker scan moved to
bytes 6..7 under PORT; compiled globalDL shadows synced post-texLoad for
explosion.c's g_ExplosionDisplayLists.

Result: game boots, plays intro music, and renders the entire ~3.5-minute
intro (logo -> gun barrel with Brosnan -> cast screen) at ~59 fps. New
blocker D69: load_bg_file faults on first stage load - BG-file headers are
N64 big-endian segment-0x0F offsets read LE on PC. Full analysis + strategy
in PCPortResearch.md §F/D69; HANDOFF rewritten for the D69 session.
2026-08-24 03:27:29 -04:00
James Dansereau 2c0c0515dd PC port: phase 2 — handoff for D51 session: findings D50/D51, rewritten HANDOFF + research §G/§H
- §F/D50: Plan B execution record (emit pass, plumbing, lang/font fixups,
  legal-screen UB, texCopyGdls byte proof)
- §F/D51: current blocker — import_texture_i8 via main-DL G_TEXRECT lazy
  tile-0 upload; GE opcode space facts (non-F3DEX numbering, G_TEXRECT=0xE4)
- HANDOFF.md rewritten: Task 1 = live-breakpoint gdb recipe for D51,
  Task 2 = pixel assert + soak, Task 3 = Tbg_*_stanZ backgrounds
- AGENTS.md phase status updated (D43/Plan B closed, D51 open)
2026-08-23 10:22:09 -04:00
James Dansereau 330b2bae46 PC port: phase 2 — Plan B executed (D50): offline model sidecars + port plumbing; texCopyGdls w1 fix
- tools_pc/d43_emit.py: full emit pass, 512/512 round-trip validated;
  data/pcmodels-<region>/pcmodels.bin + manifest.csv (gitignored, regenerate)
- port/src/pcmodels.c(+h): cart-extension reservation, sidecar copy, one-shot
  file_resource_table patch; romdata.c/h extended
- objecthandler_2.c: #ifdef PORT hook — pcmodelsPatchTable() + poolRemaining=0
  reset (closes D48.3/R6 reload hazard)
- language.c/textrelated.c: runtime C fixups for lang-bank BE offset tables
  (romdataFixupLangBank) and font re-layout (romdataFontPcSize/FixupFont)
- front.c: legal-screen uninitialized-pointer UB seed (N64 survived by luck)
- tex.c (D50.6): texCopyGdls copied only w0 of each 16B Gfx slot on PC —
  compaction scratch kept stale mempool w1s -> garbage G_SETTIMG -> OOB in
  import_texture_rgba16. Full-slot copy under #ifdef PORT; byte-proven vs
  PlegalpageZ sidecar (w0s match, w1s stale). Model GDLs now execute.
2026-08-23 10:22:03 -04:00
James Dansereau 8f7ab8ec69 PC port: phase 2 — D49 review: Plan B verified (R1-R8 all confirmed), green-lit for execution
- R1 RZ format: 512/512 files [0x11 0x72]+raw deflate; sum round8(C) = 1,277,088 B (exact D48 match)
- R2 romCopy = gated host memcpy; no 0x10C00000 limit; sidecar region needs only reservation+validity extension
- R3 file_resource_table is the single chokepoint; no direct model-symbol derefs outside table
- R4 obInit runs once (boss.c:179) before any model load; lazy one-shot patch safe
- R5 fit quantified: max round8(C)+8 = 0x4228; worst-case P_final = 0x16F74 vs 0x24C400 bank (25x); all dst!=0 buffers pass vs current constants (chainbound re-run)
- R6 reload hazard real + data-capable (PlegalpageZ example); one-line reset pending, closes it
- R7 per-region sidecars work (region build-time-fixed; EU=465 files); notes: no JP ROM candidate in romdataInit
- R8 emission spec complete; fresh C probe (exact build flags) re-verifies all 15 PC record sizes + promotion offsets — no drift; added explicit bswap16 rule for u16/s16 record scalars to HANDOFF Verified facts
2026-08-22 19:24:42 -04:00
James Dansereau 73ea03734d PC port: phase 2 — D48 review: re-plan D43 as offline pre-conversion (Plan B)
- D48 finding in PCPortResearch.md §F: d43_convert.py does not emit bytes
  (layout+remap validation only); Plan B = Python emit pass -> data/pcmodels-<region>/
  RZ sidecars served via port-layer hw_address/rom_size table patch (no C
  converter); Plan A kept as fallback with corrected staging guard (D_N64+D_PC<=avail)
- Moved all 23 d43_*.py investigation scripts from gitignored build-pc/ to tracked
  tools_pc/ (spec is now versioned); d43_cover.py path ref updated; converter
  re-run from new location: ALL CLEAN
- HANDOFF.md rewritten as two-session prompt: Session A = independent review of
  Plan B (8 claims, each with falsification criterion -> finding D49), Session B =
  execute (emit pass + round-trip validator, romdata plumbing, one-line hook,
  pixel-assert soak); verified-facts converter spec preserved
- AGENTS.md phase status updated to D48 re-plan
2026-08-22 18:43:04 -04:00
James Dansereau 181f6db07d PC port: phase 2 — D43 mechanical ABI edits + D46 buffer sizing + fast3d seg-5 case; converter spec validated (D47)
- bondtypes.h/model.c/chr.c/objecthandler_2.c/tex.c: PORT-guarded pointer-width
  and Gfx-size fixes (D43 step 2, uncommitted since prior session)
- front.c cast-screen bufferRemaining 0x1C000->0x25000, initmenus.c logo
  buffer 0x7C000->0x85000 (D46 strict bound)
- gfx_pc.cpp seg_addr: resolve raw 0x05xxxxxx via segmentPointers[5] + low24
  (unmarked segmented addresses, ROM convention)
- docs: D47 finding — converter contract finalized (compaction delta=R-P,
  accessors at 0x80090000, G_VTX GBI1 encoding, opcode-aware w1 remap rule,
  vertex formats, no embedded blobs, zero-vtx-array rule, staging contract,
  reload-hazard poolRemaining reset); HANDOFF rewritten around the remaining
  C implementation
- build-pc/d43_convert.py (gitignored, local): reference converter + validator —
  full N64->PC conversion spec run on all 512 ROM files, ALL CLEAN (max D_PC=0xB7E0)
2026-08-22 18:24:31 -04:00
James Dansereau f298dc6703 PC port: phase 2 — D43 investigation complete (handoff + plan) 2026-08-22 10:06:14 -04:00
James Dansereau 3458a4e868 PC port: fix crash-handler Phase 2 self-fault, auto-backtrace on crash (D44) 2026-08-22 08:24:11 -04:00
James Dansereau 89653cdcbb PC port: document PD-port copy-candidate audit (§2.4) — standing reference, D43 near-analogue, Phase 3/4 copy targets 2026-08-22 08:17:34 -04:00
James Dansereau 95b6e41a0c PC port: handoff for D43 session — dev-loop measurements, D44 crash-handler finding 2026-08-22 08:09:34 -04:00
James Dansereau 7e81816539 PC port: note D40-class pool audit (negative) in handoff 2026-08-22 04:09:20 -04:00
James Dansereau 98412c281c PC port: Phase 1.5 done, first frames render (D38-D42)
- D38: implicit function declarations truncated pointer returns on x86-64;
  added port/include/pc_protos.h prototype shim (398 decls, C-only, anchored
  in port/shim/PR/ucode.h) + pc_netorder.c for ntohl/ntohs; build warning-clean.
- D39: Globalimagetable rebasing - per-symbol offsets + GIMG_OFF() in
  image_bank.c (== N64 (u32)&sym incl. the 0x02000000 base); segment end
  markers extended to the true 0x13F8 size (CSV entry truncated by 0x930).
- D40: g_ModelHitEntries pool sized for the 40-byte PC ModelHitEntry stride
  (was a 12KB .bss overflow clobbering is_ramrom_flag -> demo-replay SIGSEGV).
- D41: release the GL context from the host main thread after videoInit so
  the game scheduler thread can bind it (WGL single-thread currency; this
  SDL2 build's MakeCurrent return value is an inverted ABI artifact).
- D42: rsp.c g_gfxTaskSettingsList XOR toggle -> explicit toggle on x86-64.

Boot now renders real frames ([NOTE] frame N rendered in ... us). Next
blocker D43 (model-file loading ABI mismatch) documented in PCPortResearch.md
section F/H; HANDOFF.md rewritten for it.
2026-08-22 04:07:07 -04:00
James Dansereau b3000825f2 PC port: phase 1.5 — handoff notes updated for D33 implementation session
Record ROM-dump verification (No-Intro [!] byte-identical, CRCs re-checked)
in AGENTS.md phase status, PCPortResearch §H state, and HANDOFF current
state, so the next session starts from a verified foundation.
2026-08-21 20:30:50 -04:00
James Dansereau bd56cff073 PC port: phase 1.5 — ROM dump verification groundwork
No-Intro 'GoldenEye 007 (U) [!].z64' placed in data/ and verified
byte-identical (MD5 70c52588...) to the dev ROM copies
(data/ge007.ntsc-final.z64, baserom.u.z64). Header magic/title/country
check out; embedded CRCs DCBC50D1/09FD1AA3 re-computed and matched via
tools/n64cksum.c's CIC-6102 routine.

Adds tools_pc/romverify.c: one-shot integrity check (size, GE header
magic at 0x00, title, country byte, CRC1/CRC2) so any future ROM swap
is detectable in seconds. Fingerprint + provenance recorded in
docs/HANDOFF.md; gitignore the built romverify.exe.
2026-08-21 20:29:03 -04:00
James Dansereau 8482ba3ed1 PC port: Phase 1.5 — root-cause D32 Part B (D33): ROM big-endian
structured fields + x86-64 loop-stride bug; fix designed, handoff updated

- Finding D33 in docs/PCPortResearch.md: the .z64 stores multi-byte
  structured fields in big-endian byte order (bit-packed streams raw);
  per-field load-time transform rule (u32 bswap32 / u16 bswap16 / u8
  identity) validated over all 173 animation entries; full blob layout
  mapped; exact reproduction of the gdb-observed garbage values.
- Second root cause: expand_ani_table_entries iterates s32** (+8 bytes/iter
  on x86-64 vs +4 on N64), rebasing only even-indexed entries (verified in
  compiled binary). Fix design: romdataFixupAnimationData() in port/, one
  call between romCopy and expand, s32* loop stride fix.
- docs/HANDOFF.md rewritten with the implementation task; AGENTS.md phase
  status updated. tools_pc/disasm.py (BE ROM research tool) added.
2026-08-21 20:18:38 -04:00
James Dansereau ab61e4be4a PC port: add paste-ready session handoff brief (docs/HANDOFF.md)
Summarizes Phase 1.5 state, the D32 Part B immediate task + gdb method, the
standing ABI/layout fix procedure, and environment gotchas for a fresh agent
session. AGENTS.md now points to it for new sessions.
2026-08-21 16:03:32 -04:00
James Dansereau dbeee7353f PC port: Phase 1.5 — fix langInit decompression (D31), start D32 ABI layout
- D31 (resolved): replace GE's gzip-1.2.4 inflate with real-zlib
  decompressdata() in port/src/rzdecomp.c; exclude src/game/decompress.c +
  zlib.c from the PC build. Root cause: struct huft is 8B on MIPS but 16B on
  x86-64, overflowing load_resource's fixed 0x2100 buffer and clobbering its
  frame/retaddr on the first file load. mainThread now boots past all language
  files into bossInitMainthreadData().

- D32 (in progress): ROM-serialized structs with 32-bit pointer fields
  misalign when read as 64-bit pointers. Part A done: ModelAnimation's
  bitDescriptors/bitStream are now u32 (N64 layout preserved) with casts at the
  two use sites in model.c. Part B (load-time rebase in expand_ani_table_entries)
  is open — see docs §F/D32 + §H.

- Docs: D31 resolved, D32 recorded (A done / B open), Phase 1.5 added, new §H
  handoff section. AGENTS.md: refined non-negotiable #2 to "game logic is
  unmodified" with a narrow ABI/layout exception; refreshed phase status.
2026-08-21 16:01:49 -04:00
James Dansereau 13ca03a6db PC port: Phase 2 — real threads, dual-mapped DRAM, fast3d integration
- Replace setjmp/longjmp green threads with real pthreads (8MB stacks);
  PortQueue side table (mutex+condvar) for OSMesgQueue; dedicated tick
  thread posts VI retrace + services timers (D24).
- Dual-mapped N64 DRAM: one 8MB store at V1 0x70000000 (s32-safe, all game
  RAM symbols) and V2 0x80000000 (KSEG0 mirror for offset|0x80000000
  rebuilds). Windows: CreateFileMappingW + MapViewOfFileEx; POSIX: memfd
  + MAP_FIXED. dram_syms.s pins cfb_16/_bssSegmentEnd/tlb block (D25).
- Address-width shims: PHYS_TO_K0 identity, OS_K0_TO_PHYSICAL = offset from
  V1 base, OS_PHYSICAL_TO_K0 identity; fast3d seg_addr() remaps small
  offsets into V2 (D26).
- ASLR off (-Wl,--disable-dynamicbase): absolute symbols referenced through
  pointer-typed externs (.refptr + BASE reloc) must not rebase; startup
  sanity check on image base 0x140000000 (D27).
- osPiStartDma posts its completion message on every path (fixes
  romReceiveMesg deadlock) (D29).
- SEH-free crash handler: raw fault log first, validated manual EBP-chain
  walk second (-fno-omit-frame-pointer), TerminateProcess; crashDumpThreads
  via Toolhelp32 for heartbeat hang dumps (D30).
- PD fast3d integrated (port/fast3d/), replaces gfxstub.c; real src/sched.c
  compiled; mainproc runs as a real pthread, host thread pumps SDL events.
- Docs: findings D24-D31 + Phase 2 status (section F/G).

Current state: boots through bossInitMainthreadData to langInit(), then
SIGSEGVs on the first file load — stack frame clobbered by a table of
truncated s32 pointers (D31, open). Next: attribute the write with
'set scheduler-locking on' + hardware watchpoint.
2026-08-21 14:42:19 -04:00
James Dansereau 2aad171344 PC port: Phase 1 boot-to-window (ROM load + map + SDL2 clear loop)
- scripts/gen_romassets.py: emit absolute cart-address asset symbols
  (0x10000000 + rom_offset) from filelist.<r>.csv + the asset .s files,
  in file_resource_table.inc.c order. US region (romassets_u.s).
- romdata.c: load/validate the GE-specific header (magic, name @0x20,
  cart ID @0x3C, country @0x3E) and map the .z64 at cart base 0x10000000
  so every &asset-symbol is a live host pointer.
- libultra.c: osPiStartDma/osPiRawStartDma now service reads as
  bounds-checked memcpys from the mapped ROM (synchronous completion).
- system.c: implement sysResolvePath ($S data dir, $E exe dir).
- video.c: real SDL2 window + GL context + clear/swap frame loop with
  FPS accounting; main.c runs a Phase 1 demo loop (ESC quits) until the
  software RSP/scheduler can drive mainproc().
- CMakeLists: project(... ASM), wire romassets_<region>.s, drop
  assetstubs.c (replaced by generated symbols); n64stubs.c keeps only
  pure-RAM segment markers.
- docs: findings D18-D23 (header offsets, BE music table, .rz tracks,
  scanner-prefixed manifest names, CMake ASM gotcha, absolute-symbol
  approach).

Verified: baserom.u.z64 (12MB) passes validation and maps at 0x10000000;
window opens and renders until killed; asset offsets spot-checked against
ROM bytes (music bank seqCount=63 BE, first track offset 508).
2026-08-20 22:19:45 -04:00
James Dansereau 5ea46fabc1 PC port: full compile + link milestone (ge007.x86_64.exe builds clean)
All ~235 translation units now compile and the target links under MSYS2
GCC 16 + CMake/Ninja (ninja ge007 -k 0: 236/236, zero errors). N64 build
untouched; every src/ change is -DPORT-gated.

Portability fixes (findings D7-D17 in docs/PCPortResearch.md section 11):
- AI X-macro system: SWITCH() call sites activated to the author's
  hand-written byte-identical references under PORT; port/shim/
  bondaicommands.h + bondconstants.h + CPPLib.h re-emit the IDO paste
  tricks (isSubroutine, file-record macros, generated CALL) GCC-clean.
- 64-bit pointer-in-word initializers: monAnim jump targets now go
  through an index table resolved by the interpreter (chrai.h/propobj.c);
  UsetuplenZ.c credits pointer PORT-gated to 0.
- IDO printf engine compiled for PC (xprintf/xlitob/xldtob) with a
  shadow xstdio.h resolving the _Printf prototype clash.
- Host-libc gaps stubbed in port/: bcopy/bzero, __libm_qnan_f,
  tlbmanage*, g_ViXScales/YScales; chrObjRandom PRNG ported from .s.

Asset data strategy (D16): single-file assets compiled for real
(animation tables, fonts, textures, rareware logo); obseg per-file .c
excluded (colliding file-local globals the N64 build isolates via
per-ELF linking); 758 ROM-derived table symbols + ramrom_* + segment
markers stubbed in port/src/assetstubs.c / n64stubs.c.

fast3d is a no-op placeholder (port/src/gfxstub.c) until Phase 2 brings
in the real software RSP. Binary not yet runnable end-to-end: ROM
loading in romdata.c is still a TODO.
2026-08-20 20:27:06 -04:00
James Dansereau cd6cc158b6 Add PC port scaffolding (CMake build + port layer)
Initial scaffolding for a PC port of GoldenEye 007, modeled on the
Perfect Dark PC port (same Rare "Indy" engine). The N64 build
(Makefile + IDA) is untouched; the PC build is a parallel CMake build.

Build:
- CMakeLists.txt: SDL2 + zlib + OpenGL. Compiles src/game/*.c + curated
  engine files + libultra audio/gu + port/. Per-region macro sets
  (ROMID US/EU/JP) mirror the N64 Makefile's LCDEFS exactly.
- build-pc.sh, cmake/: configure+build helper and target/SDL2 modules.

Port layer (port/):
- video/audio/input/fs/romdata/config/system/mixer: stubs with
  TODO(Phase N) markers and correct interfaces.
- libultra.c: libultra OS shims (message queues, VI, PI, AI, EEPROM,
  controller, PFS/motor no-ops, region-gated osTvType).
- gesched.c: software-RSP scheduler replacing src/sched.c task submission.
- n64stubs.c: inert stubs for N64-boot + rmon symbols.
- random.c: PRNG ported verbatim from src/random.s (bit-exact).
- ucode.c: RSP/ASP/GSP microcode segment markers (dummies; Phase 3 TODO).

Docs:
- docs/PCPortResearch.md: architecture, GE-specific deltas, phased plan,
  and the scaffolding review (findings A1-A4/B1-B4/C1-C2 + resolutions).
- PORTING.md: top-level entry point.

See docs/PCPortResearch.md for the full plan.
2026-08-17 19:17:06 -04:00