forked from Ports/goldeneye-pc-port
2-4 player split-screen (#99): D416-D419, D422, D428, D448, D450
Virtual seats, per-seat camera and input routing, aim modes, hot-plugged controllers reopening via a coalesced rescan (D450, dolent's PR #122, merged as the original commit). Rule-2 sign-off for D417. Refs: #99 D416 D417 D418 D419 D422 D428 D448 D450 #122
This commit is contained in:
commit
00fdd94c8d
24 files changed
+1090
-226
No files matched your search
@@ -694,6 +694,14 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | **CLOSED (2026-09-28 bookkeeping: tank board/exit accepted by the user; shipped in v0.4.0).** Earlier: PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). |
|
||||
| D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | FIXED 2026-09-30 (SkipIntro hook gated to first boot; live dossier check owed). |
|
||||
| D415 | **Shooting a light fixture does not "kill" its flickering light (Bunker, Caverns): the fixture takes the hit but the flicker keeps going (community report, issue #87 comment by the reporter, 2026-09-28, 00 Agent playthrough of v0.4.0).** — full `## D415` entry at file tail | FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed. |
|
||||
| D416 | **2-player split-screen crashes (issue #99, v0.3.0 `4827bc8`, fault addr 0xa43a0ed4, PC 0x14009e758): MP setup files `Ump_setup*Z` were never converted N64→PC** — full `## D416` entry at file tail | RESOLVED in-harness (converter + D417 + D418 fix the 2P crash and split-screen flicker; 18/18 2P/3P/4P x 6-stage load runs clean; live pad play still owed). Also D417, D418.
|
||||
| D417 | **`enum PROP` unsigned on GCC: `prop >= 0` passed for `getPropForHeldItem()`'s -1 (fist) -> `modelLoad(-1)` null deref in any 2P+ stage (player.c `sub_GAME_7F09B398`)** — full `## D417` entry at file tail | RESOLVED (src/bondconstants.h `PROP__PORT_SIGNED = -1`, D3/D142 precedent; **Rule-2 review owed**). |
|
||||
| D418 | **Split-screen: fast3d safe-area crop stretched the last-set (half-height) viewport over the whole window, so each player's view filled the screen and the last-drawn (shuffled) player won -> alternating full-screen flicker** — full `## D418` entry at file tail | RESOLVED (`gfx_set_split_screen`, port-only). |
|
||||
| D419 | **Split-screen input shared between players / P1 lost solo controls: poll thread used g_CurrentPlayer for per-slot aim/stance/style/use** — full `## D419` entry at file tail | RESOLVED (per-slot `slotPlayer()`, port-only; no current-player swaps from the poll thread; live-tested OK). |
|
||||
| D420 | **MP score / stats screen shows an odd big number (issue #99)** — `char rankbuffer[4]` overrun ("Rank: 1st") clobbered the adjacent `scores[]` (D8 class); widened under `#ifdef PORT` — full `## D420` entry at file tail | FIXED 2026-10-01 (`mp/virtual-seats`); 2P/3P/4P watch-menu runs show 0s. |
|
||||
| D421 | **Gadget cycling (pad B / gadget key) disabled in 2+ player matches** — full `## D421` entry at file tail | CLOSED 2026-10-01: moot, no MP inventory holds a gadget (mp_weapon.c); hook reviewed + reverted. |
|
||||
| D422 | **P2+ have no centred-aim mode in split-screen (single-slot aim state)** — full `## D422` entry at file tail | FIXED in tree (commit `ac92e0ab`: per-slot aim state `s_aimSt[4]`, `aimModeGet()` returns the configured mode for every slot); live split-screen verification of centred aim on slot 1+ still owed (needs a non-slot-0 aim device). |
|
||||
| D423 | **Unidentified black arrowhead in P2's split-screen view (stage 38)** — full `## D423` entry at file tail | CLOSED 2026-10-01: faithful (level shadow at a doorway top), maintainer-confirmed live. |
|
||||
| D424 | **`WeaponStats.RecoilSpeed`/`b44[]` byte order: no grenade-launcher recoil, shortened recoil on every gun (issue #118).** — full `## D424` entry at file tail | FIXED in tree (`WS_B44`, port-guarded); user-verified vs GEPD/1964 2026-09-29. |
|
||||
| D425 | **Tank run-over death sound loops: `(chrflags << 7) >= 0` is always true on unsigned GCC enum (issue #115).** — full `## D425` entry at file tail | FIXED (2026-09-29 bookkeeping: use-site `(s32)` cast; user-verified vs GEPD/1964 2026-09-29, #115). Earlier: FIXED in tree (use-site `(s32)` cast); live check owed. |
|
||||
| D426 | **Tank cannot be steered with the mouse out of aim mode (issue #116).** — full `## D426` entry at file tail | RESOLVED (user 2026-09-29: tank controls now match GEPD/1964; see D429). |
|
||||
@@ -717,9 +725,15 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121).
|
||||
| D447 | **Pillarbox output mode (`Video.AspectMode` = Original) for the Original N64 preset; crop overscan no longer cancels Wide/Cinema.** — full `## D447` entry at file tail | FIXED (build + 3 captures verified; Wide/Cinema, 16:9 Ratio, mouse and 1964 comparison owed to a human) |
|
||||
| D445 | **Crouched-rifle firing animation (`crouched_rifle_firing_animation_groupA`) never resolved on PC -> SIGSEGV in modelConstrainOrWrapAnimFrame when an MP player crouch-fires a rifle (2026-09-28).** — full `## D445` entry at file tail | FIXED — layout only (§D5 linker adjacency, D164 class): `crouched_rifle_firing_animation_group1` has no zero terminator and on N64 falls through into `...groupA` (source-order .data); GCC places groupA first, so groupA[0] kept the raw anim offset 0xB84. `#ifdef PORT` one array {right, left, terminator} + `groupA` alias (chr.c/chr.h); N64 resolution restored. |
|
||||
| D446 | **Couch 2P watch-menu text (the MP pause page) differs by +-1-2 per channel between builds whose only difference is data layout (2026-09-29).** — full `## D446` entry at file tail | FIXED — port-only (fast3d): `GL_TEXTURE_MAX_ANISOTROPY` is per-texture-object state that `gfx_opengl_set_sampler_parameters` set only when mipmapped and never reset; a GL texture id freed by `gfx_texture_cache_delete_range` (unordered_map order = texture address order) and reused for a non-mipmapped glyph kept 4x. Now always written (level or 1). Render-only; sim never affected. |
|
||||
| D448 | **Split-screen pad seats: `pads[]` indexed by seat and keyed by SDL instance id; seats compact outside a match and stay reserved during one (an unplug no longer shifts other players or flips keyboard/mouse-as-P1 routing; a rescan no longer reopens attached pads).** — full `## D448` entry at file tail | PARTIAL (committed 918ac5f9 on `feat/mp-splitscreen-99`, build + 2P smoke OK; plug/unplug behaviour not exercised at runtime). |
|
||||
| D456 | **Font-pointer / text-string-pointer slots retyped from `s32` to real pointers (ABI/layout exception; front.c, options.c, language.c, bondview*, mpmenu.c, propobj.c:3909 alias replaced by named field).** `frontPrintText`/`display_aligned_white_text_to_screen` font params, `copy_1st/2ndfonttable`, `setFontTables`, ~26 options.c locals, `g_LangBanks[]` + `langGet` result, `player.ptr_text_*_mp_award`. front.c 186->2, options.c 139->0 int-conversion warnings. FIXED (static + 1 launch; not gameplay-verified). |
|
||||
| D453 | **memp allocator API made 64-bit clean (D441 step 0; ABI/layout exception, §A1).** `mempCheckMemflagTokens` took the arena start as `s32`, `mempSetBankStarts` carried it in an `s32 mempStart`, and `mempAllocPackedBytesInBank` returned a pointer as `u32`; all truncate/sign-extend an arena at or above 0x80000000. Widened to `uintptr_t`/`void*` under `#ifdef PORT` (N64 arm unchanged). mema.c NOT changed (structural, see entry). | 2026-09-30 | fix/abi-memp-widen | [H] for memp, mema deferred |
|
||||
| D451 | **Automatic-fire gate at unsteady frame rates (D427 follow-up) and D173 residual.** — full `## D451` entry at file tail | FIXED (**RULE-2-SIGNOFF**, maintainer 2026-09-30: covered by the D427 grant; tick-crossing gate, `gunfire.c` `d427Gate`); headless A/B at FpsCap 45 measured; maintainer hold-fire test at 60/30 fps showed no perceptible difference (expected: identical at steady rates). D173 not the same class (see entry). |
|
||||
| D463 | **fast3d divided by zero (0xc0000094) importing a texture for a zero-area TextureRectangle on a tile with `line = 0`: the outlined-text path drew a zeroed font glyph (`w=h=0`, `pixeldata=NULL`), found by the scripted 3P/4P stability sweep (crash rate roughly 2 in 14 on the Library stage; section D).** Full `## D463` entry at file tail | FIXED (port-layer guard in `import_texture`; 8 repeats of the crashing config + the 18-run sweep + solo `-level_09` + menu boot all rc=0 with no crash; the guard fired in 3 of 8 repeats). |
|
||||
| D464 | **`GE_MEMPREDZONE=1`: 64-byte red-zone checker for the `memp` bump allocator (all banks), side table + checks at level unload / every 60 frames / orderly quit; re-introducing the D462 `0x14` stride is detected (falsifier PASS); 21 solo levels + menu clean (0 hits); MP matrix not run (GE_STARTMP lives only on feat/mp-splitscreen-99).** Full `## D464` entry at file tail | TOOL LANDED (local, `feat/memp-redzone`); MP sweep owed. |
|
||||
| D407 | **Tanks cannot be boarded/exited on PC — the v0.4.0 use/reload split (D378/D393) removed the B-button tap the engine's tank handlers in `bondview2.c` consume (user report 2026-09-28, Runway/Streets)** — full `## D407` entry at file tail | PARTIAL (fix landed in v0.4.0, port-only): present `GE_CONT_B` on the use (E / pad A) rising edge only while `g_PlayerIsInTank == 1` or `g_BondCanEnterTank != 0`; E keeps its D378 no-reload-fallback semantics elsewhere, N64 layout unaffected. D407(b) same cycle: front PC Options page-edge highlight clamp + mouse-wheel / W-S paging (wheel queue consumed on the menu, D223); its wheel mapping shipped inverted and was fixed to match W/S (wheel up = step up). D407(c): board-animation lockout, menu-accept B gate in tank states, in-tank aim routed through the legacy velocity stick + `Input.TankAimScale` knob. RESOLVED 2026-09-28: all port-only (src/game zero-diff), 10 TANKDBG probes stripped, release binary verified clean, user live tank drive signed off (board + exit OK, aim feel good). |
|
||||
| D408 | **`Game.SkipIntro` skips the post-mission failure dossier: with it on, entering a level then aborting (watch Z+A) or dying (KIA) returns straight to the menus — no REPORT / "Mission status: KILLED IN ACTION / ABORTED" screen** (user report 2026-09-28; save/AllUnlocked ruled out) — full `## D408` entry at file tail | OPEN (cosmetic, not root-caused past the handoff site). v0.4.0 decision: SkipIntro stays EXPERIMENTAL / not recommended for regular users — F10 row relabelled "Skip intro (EXPERIMENTAL)". |
|
||||
| D450 | **Unplugging and replugging a controller was not picked up (user, GameSir-G7 Pro; 2026-09-29).** — full `## D450` entry at file tail | FIXED — port-only: the render thread's `gfx_sdl_handle_events` drains the same SDL queue as video.c's host pump but had no case for `SDL_CONTROLLERDEVICEADDED/REMOVED`, so it usually swallowed them and `inputRescanPads` never ran. Both pumps now post a request; the rescan runs at the top of `inputUpdate`, on the thread that reads the pads. |
|
||||
|
||||
|
||||
Phase 2 replaced the Phase-1 demo loop with the real `mainproc()` on real OS
|
||||
@@ -15632,6 +15646,89 @@ flickering fixture), then repro on PC.
|
||||
**Current status (2026-09-29 bookkeeping):** the Status line above is historical; the authoritative verdict is the §F row — FIXED (2026-09-29 bookkeeping: root-caused and fixed as D430 + D431, user-verified vs GEPD/1964 on Caverns 2026-09-29; #119). Earlier: ROOT-CAUSED as D430 (see there); fix in tree, live check owed.
|
||||
|
||||
|
||||
## D416 — Local split-screen multiplayer crash: MP setup files never converted (issue #99)
|
||||
|
||||
**Report:** issue #99, 2P split screen, v0.3.0 `4827bc8`, fault addr `0xa43a0ed4`, crash PC `0x14009e758`.
|
||||
|
||||
**Symbolised (2026-09-29, `dist/goldeneye-pc-port-0.3.0-win64` exe, addr2line):** `load_proptype` (prop.c) called from `proplvreset2` (prop.c) — the propDefs record walk over `g_CurrentSetup`, i.e. the setup-file load, not menu code.
|
||||
|
||||
**Root cause:** `prop.c` loads `"U" + "mp_" + <stage>` (`Ump_setup*Z`) whenever `getPlayerCount() >= 2`. `tools_pc/d88_emit.py` (and `d88_propdefs.py`, `d88_propdef_scan.py`, `d125_check.py`) only matched `Usetup*Z`, so the 13 MP setups in `file_resource_table.inc.c` came out of pccg as raw big-endian N64 data and the walk read garbage. (Not the D190 class: D190 is closed as `-level_45` being MP-only with no solo setup.)
|
||||
|
||||
**Fix (tools only, no game code):** single predicate `is_setup_name()` = (`Usetup`|`Ump_setup`) + `Z`, used for the name list, the `--regen` row-drop, and the analysis tools. `convert_usetup()`'s full-tiling assertion is the gate that MP files share the solo format.
|
||||
|
||||
**Verified:** `d88_emit.py ntsc-final --regen` → `converted: 34/34`, ALL CHECKS PASSED; `d125_check.py` MATCH on all 13 MP + 21 solo files; pccg manifest 73→86 rows with all 73 pre-existing rows byte-identical. PAL/JP not run (NTSC ROM only).
|
||||
|
||||
**Follow-up (2026-09-29):** in-game 2P now runs (see D417, D418). Also added: `Input.MPMode` (Auto/PadsOnly/KbmP1) + slot->pad shift so one pad or kbm+pad can be P1/P2 and the game's `joyGetControllerCount() >= 2` MP gate opens; `GE_MPVIRT` / `GE_STARTMP` harness flags (this machine has 0 SDL gamepads). Input layer is still single-player shaped (see memory: mp-input-layer-direction). Separately, PC input hooks are pad-0-only and `input.c` re-asserts control type on the current player (can rewrite another player's control style in MP) — port-layer work planned for v0.5.0.
|
||||
|
||||
**Status:** RESOLVED in-harness (see header row).
|
||||
|
||||
|
||||
## D417 — `enum PROP` unsigned: fist held-prop lookup (-1) passes `>= 0` and crashes 2P+ (issue #99)
|
||||
|
||||
**Crash (gdb, 2026-09-29):** `modelLoad(modelid=-1)` <- `something_with_generating_object(propid=-1, itemid=ITEM_FIST)` <- `sub_GAME_7F09B398` (player.c) <- `gunTickHandState` <- `bondviewProcessInput`. Fault: `PitemZ_entries[-1].header->RootNode`, header NULL.
|
||||
|
||||
**Root cause:** `getPropForHeldItem()` returns -1 for items with no held model (fist, taser). `sub_GAME_7F09B398` guards with `if (prop >= 0)`; `PROP` is an all-non-negative enum, so GCC gives it an unsigned type and -1 passes. Solo never runs this path (only other players' visible weapons need it). Same class as porting-notes D3 / D142.
|
||||
|
||||
**Fix:** `PROP__PORT_SIGNED = -1` sentinel under `#ifdef PORT` at the head of `enum PROP` (`src/bondconstants.h`), following the `SP_LEVEL__PORT_SIGNED` precedent. `PROP_ALARM1` stays 0, sizeof unchanged, no stored value changes. Also corrects `drop_inventory` (`propid >= 0`). **This is a `src/game`-header edit: Rule-2 review owed.**
|
||||
|
||||
**Status:** RESOLVED.
|
||||
|
||||
## D418 — Split-screen alternating full-screen views: safe-area crop remapped sub-rect viewports (issue #99)
|
||||
|
||||
**Symptom (user, live):** in 2P the view flickers between both players constantly. GE_MPDUMP (temp) frame capture: each frame is ONE player's view filling the whole window, alternating in shuffled draw order (`get_nth_player_from_shuffled`) — no split visible.
|
||||
|
||||
**Root cause:** `gfx_adjust_viewport_or_scissor` (fast3d) maps the last-set SP viewport's Y range onto the whole window when `Video.SafeAreaCrop` is on (default; designed for the solo 220-line viewport). In 2P each player's viewport is 109 lines, so each was stretched to full height and the last drawn covered the other.
|
||||
|
||||
**Fix (port only):** `gfx_set_split_screen()` turns the crop (Y and X margin) off while a 2+ player stage runs; `videoSyncSplitScreen()` (video.c) sets it from `getPlayerCount()` + stage != title, called before every `gfx_run` — **including the scheduler's direct call in `libultra.c`**, which bypasses `videoSubmitCommands` (my first attempt hooked only that and had no effect: worth remembering, see porting-notes). Verified by frame capture: two stable stacked views over 4 consecutive frames, original N64 letterbox, viewports y=119/230 h=109; solo golden gate 3/3.
|
||||
|
||||
**Status:** RESOLVED (harness); live pad play + HUD/widescreen (plan 2c, D334/D335 in MP) still owed.
|
||||
|
||||
**2026-09-30 note (merge of release/v0.4.1, D447 interaction):** D447 gated the crop cache so only viewports >= 90% of the canvas height define the crop band (Wide/Cinema and split halves keep their own letterbox) and added an output rect (Original aspect = centred 4:3/16:9 rect, `gfx_current_game_window_viewport` = that rect). The two compose without overlap: in a 2+ player stage D418's `g_split_screen` still bypasses the crop entirely (`safeTop/safeHeight = SCREEN_HEIGHT`, no X margin), so the N64 split layout maps 1:1 onto the canvas; D447's seed/gating only touches the cached band, which is unused while split. The canvas itself is then placed in the window or, in Original mode, inside the pillarboxed output rect by `gfx_start_frame`. Merge conflict was textual only (adjacent globals in gfx_pc.cpp).
|
||||
|
||||
## D419 — Split-screen input shared between players / P1 lost solo controls (issue #99)
|
||||
|
||||
**Symptom (user, live):** in a 2P match one player's input drove both views, and P1 keyboard/mouse or pad did not behave like solo (no mouse look, no SOLITARE layout, no crouch/use/reload).
|
||||
|
||||
**Root cause:** the slot plumbing (`contSnapshotFromKeyboard` polls 0..3, player N reads controller N) was already per-slot, but `inputComputePad` ran on the scheduler thread and read/wrote `g_CurrentPlayer` — whichever player the game thread last selected — for aim, stance, style and use/reload. The D416 stopgap disabled those paths in a match, which removed them from P1 too.
|
||||
|
||||
**Fix (port only, `port/src/input.c`):** `slotPlayer()` resolves the player that owns the slot being polled (`s_slot`, set by a thin `inputComputePad` wrapper); all player-struct access goes through it. In a 2+ player stage the poll thread NEVER calls a game entry point that acts on `g_CurrentPlayer`, and never re-points the current-player globals: control style is written straight onto the slot's player (mirror of `cur_player_set_control_type`), the crouch weapon check reads `p->hands[GUNRIGHT].weaponnum`, use/reload go out as the native B button, gadget cycling is solo-only. Crouch state is per slot; slots 1+ get pad A/X = B, stick click = crouch; slots 1+ use the N64 aim integrator (centred aim state is single-slot); `portMouseAimPdGetTurn` applies to player 0 only; unused slots return neutral.
|
||||
|
||||
**Lesson (first attempt hung the game):** the first version wrapped those calls in a save/swap/restore of `g_CurrentPlayer`/`player_num`/`g_playerPerm`. The poll thread is a real thread, not the game thread, and the style check ran the swap every poll. When the game thread did its own `set_cur_player` inside the window, the restore left it on the wrong player and `bondinvCycleForward` (bondinv.c:523) looped forever (circular list walked against another player's list head; kernel heartbeat dump showed mainThread spinning there after a respawn). Never swap the current-player globals from the poll thread. See porting-notes.
|
||||
|
||||
**Status:** RESOLVED (user live-tested M&K P1 + pad P2 and the kill/respawn sequence, 2026-09-29). Open: P2+ centred aim; gadget cycling in matches; MP score screen shows an odd big number (not investigated).
|
||||
|
||||
## D420 — MP score / stats screen shows an odd big number (issue #99)
|
||||
|
||||
**FIXED 2026-10-01 (`mp/virtual-seats`): a D8-class stack overrun, not a scoring bug.** Reproduced headlessly (2P, stage 38, `GE_STARTMP=2 GE_MPVIRT=1 GE_INPUTSCRIPT="400:START"`, frame 450): the watch menu's Scores page showed `1932599354` / `116` at 0 kills. A temp probe in `display_text_for_playerdata_on_MP_menu` showed `get_points_for_mp_player(0/1)` = 0 / 0 while the passed `points` were the garbage values. `1932599354` = 0x7331203A = bytes `": 1s"` and `116` = `'t'`: the tail of **"Rank: 1st"**. `mp_watch_menu_display` (mpmenu.c) declares `char rankbuffer[4]` (decomp size) but `write_playerrank_to_buffer` writes "Rank: 1st" (10 bytes incl. NUL) into it, and `sprintf` writes "P<n> KILLS"/"P<n> LOSSES"; GCC places `scores[4]` right after it, so the rank text overwrote `scores[0..1]` after they were filled. On N64 the IDO frame layout absorbs the overrun (the menu shows 0). **Fix:** `rankbuffer[64]` under `#ifdef PORT` (D8 policy: pure local stack scratch, zero ABI role, same as D295's `difficultytext`); the N64 build keeps `[4]`. Not a Rule-2 change (removes a PC-only clobber). **Verified:** the same 2P frame shows 0 / 0; 2P/3P (Library)/4P watch-menu runs with pause, unpause, exit-cancel and exit-confirm show 0s in every layout, all rc=0, `GE_MEMPREDZONE` 0 hits. Not checked: the post-match awards screen (the `GE_STARTMP` harness relaunches a match instead of returning to the menus), and the maintainer's original live report (assumed to be this page). The `u16 *text` sprintf slot in `display_text_for_playerdata_on_MP_menu` (8 bytes on PC) still overruns for a score of 8+ chars (only possible far beyond any point limit); left as is.
|
||||
|
||||
**Symptom (user, live, 2026-09-29):** in a 2P match the player score / position display shows "a big number"; at match start both players show 1st place. Still present after a kill + respawn.
|
||||
|
||||
**Read so far (code only, no capture):** `write_playerrank_to_buffer` / `get_points_for_mp_player` (mpmenu.c ~903/826) rank by kills-minus-suicides with ties sharing the better rank, so 0-0 at start = both 1st is N64-faithful. A big number could be the stats screen's `shortest_inning` (init `S32_MAX` = 2147483647 in lv.c:548, only lowered on a death), but it persisted after a kill/respawn, so that is NOT confirmed. Nothing 64-bit-suspect found in the rank/points path (`kill_counts` is a plain `int[4]`); `display_text_for_playerdata_on_MP_menu` does `sprintf(&text, "%d", …)` into a `u16 *` local (8 bytes on x64, 4 on N64) and `char rankbuffer[4]` holds "1st" exactly — both worth a second look if a capture shows garbage.
|
||||
|
||||
**Next:** get the exact screen (pause/watch menu vs end-of-match) via `GE_PCDUMP` or a screenshot; identify which field prints the number; check against the 1964/N64 reference before deciding whether it is a port bug.
|
||||
|
||||
**2026-09-30 code-only pass (no fix, hypothesis unconfirmed):** the in-match number is drawn by `display_text_for_playerdata_on_MP_menu` (mpmenu.c:760) from `get_points_for_mp_player` (mpmenu.c:826) = kill_counts[] (s32, player.h:19) minus suicides + `killed_gg_owner_count*(n-2)`; all are zeroed per player in initBondDATA.c:267-269, so no uninitialised read found and the struct fields are plain s32/int (no 32->64 widening). Candidates not ruled out: (1) mpmenu.c:771 `u16 *text; sprintf(&text, "%d", points)` uses a pointer-sized local as a char buffer (4 bytes on N64, 8 on PC; a value with >7 chars overruns it and the neighbouring locals) -- any large/negative score would print big and could corrupt adjacent stack; (2) `s32 q/h2 = (s32)langGet(...)` then `(char*)q` (mpmenu.c:1408-1410, 1478-1480) truncates string pointers on 64-bit (works only while strings sit below 4 GB; the label prints correctly, so probably benign here). Next step: dump `scores[]` at mpmenu.c:1240 with GE_PCDUMP at the moment the number shows.
|
||||
|
||||
## D421 — Gadget cycling disabled in 2+ player matches
|
||||
|
||||
**CLOSED 2026-10-01: moot, no fix needed.** The pad-B cycle only selects items `ITEM_BOMBCASE..ITEM_WATCHMAGNETATTRACT` (mission gadgets). No MP weapon set (`src/game/mp_weapon.c`; every `INLINE_S_MP_WEAPON_SET` entry tallied) contains one: MP 'devices' are the timed/proximity/remote mines, which sit below `ITEM_BOMBCASE` and already cycle natively. So a match inventory never holds a gadget, and B would select nothing even with a working hook. A design review (Sonnet subagent, read-only, delegated by the maintainer) had approved a game-thread drain hook (`inputGadgetDrain()` inside `if (canCycleWeapons)` in `MoveBond`'s input step, bondview2.c ~6325, per-seat `SDL_atomic_t` one-shot, gated on alive/watch/lock/stop/game-over). It was implemented and built, then reverted unmerged once the empty gadget range was found. The early return in `inputCycleGadget()` stays. If a mode ever puts gadgets into an MP inventory, that design is the one to use. Side note from the review: solo's `inputCycleGadget()` also runs on the poll thread and mutates `hands[]` while the game thread may read it. No hang is possible in solo (`g_CurrentPlayer` never changes), but it is a latent data race; low priority.
|
||||
|
||||
`inputCycleGadget()` (port/src/input.c) acts on `g_CurrentPlayer` via game entry points with no player argument; calling it from the poll thread raced the game tick (D419 hang), so it now returns immediately in a match. Solo unchanged. Restoring it needs a game-thread hook (src/game edit, Rule-2 sign-off) or a native-button equivalent. Native N64 weapon cycling (C-buttons / pad Y) still works.
|
||||
|
||||
## D422 — No centred aim for P2+ in split-screen
|
||||
|
||||
**Update (2026-09-30, verified by code reading):** fixed by `ac92e0ab` (an ancestor of `mp/virtual-seats`): `s_aimSt[4]` with the `AIMS` slot accessor, a stateless `padDirectCompute` on `slotPlayer()`, and `aimModeGet()` no longer forces `AIMMODE_N64` for slots > 0. Slot 0 stays byte-for-byte (`AIMS` falls back to slot 0). This entry was left saying 'parked'. Untested live on slots 1+.
|
||||
|
||||
Centred aim (`aimModeGet()==AIMMODE_CENTRED`, `padDirectCompute`, `s_gepdCross*`) keeps single-slot static state, so `aimModeGet()` returns `AIMMODE_N64` for slots > 0 (port/src/input.c). P1 keeps its full aim set. Making the state per slot is mechanical but touches the aim accumulators; parked as low priority.
|
||||
|
||||
## D423 — Unidentified black arrowhead in P2's split-screen view
|
||||
|
||||
**CLOSED 2026-10-01: faithful, not a bug.** The maintainer played a live 4P Temple match (mouse/keyboard P1 + Xbox pad P2, split-screen build) and confirmed the black triangle is the level's own shadow/shading at the top of a doorway. The session exited cleanly (window closed, rc=0, no crash log).
|
||||
|
||||
**2026-10-01:** seen again headlessly in ONE frame of a 4P run on stage 38 (`GE_STARTMP=4 GE_MPVIRT=3`, scripted seats): P4's quadrant at frame 920, a flat, untextured near-black (RGB ~10-13) right-pointing triangle with sharp edges at a wall/ceiling edge; it looks in-world, not HUD. (An earlier note here also claimed P2 at frame 380; that was a thumbnail misread, and the full-res crop shows nothing.) So it is real rendered pixels and not 2P-only, but it has not been shown to be a port bug: it could be legitimate geometry, e.g. part of another player's model or a level object, until checked against 1964 or traced.
|
||||
|
||||
Seen in a `GE_PCDUMP` frame (2P, stage 38, frame 500) as a black right-pointing triangle at the left of the lower (P2) view, and again in live play (user: "not sure what it is"). Not identified: could be an in-world object/marker or a HUD/radar element drawn with a bad texture. Next: capture several frames, `GE_DTEX`/DL dump around that screen position, check whether it also appears in P1's view or in solo MP-stage loads.
|
||||
|
||||
## D424 — `WeaponStats.RecoilSpeed`/`b44[]` byte order: no recoil animation on the grenade launcher, shortened recoil on every gun (issue #118)
|
||||
|
||||
**Root cause:** `b44[4]` aliases the four bytes of the big-endian `RecoilSpeed` word (`gun.h`). Byte 0 is the MSB on N64 but the LSB on little-endian PC, so `gunfire.c` read rise/fall/retrigger/offset reversed. Grenade launcher `0x081000FF` (8,16,0,-1) read as (-1,0,16,8): `field_890 >= sp1A4+sp1A0` was true at once, so the recoil phase never ran. KF7/Klobb 4+12 -> 6+0 ticks, PP7 4+8 -> 0+3.
|
||||
@@ -15683,6 +15780,8 @@ After D426, non-aim mouse turning in the tank is good. Still open: (1) right-mou
|
||||
|
||||
## D428 second pass (2026-09-29)
|
||||
|
||||
**Split-screen update (2026-09-30):** the sway accumulator is now one per player (`s_swayThetaMdeg[4]`, `ac92e0ab`). The game-side drain in `bondview2.c` is still gated to player 0 (`get_cur_playernum() == 0`), which is correct today: `swayFeedTheta` is only called from the slot-0 mouse block, so slots > 0 are never fed. Widening the gate is only needed if a non-zero slot ever gets a mouse (a `src/game` edit inside the D428 Rule-2 block; ask for sign-off then).
|
||||
|
||||
Probe (`GE_D428`) showed the hook firing with the input clamped at 0.7 on most fast turns. The game's natural stick turn sets `speedtheta = (stick/70)^2 * fov/60` with NO ramp (max ~1.0) and integrates `vv_theta += speedtheta*dt*3.5`, so the exact inverse for a mouse turn is `deg / (3.5*dt)` clamped to `fov/60`. The first pass used the vertical axis' `delta/(2*dt)` clamped to 0.7: too weak at full speed, too strong at slow speed. Switched; user reports a slight difference turning fast, which this addresses.
|
||||
|
||||
## D433 — Automatic-gun shot sound still audibly differs slightly from GEPD/1964 (user by-ear, Caverns, 2026-09-29)
|
||||
@@ -15925,6 +16024,13 @@ About 55-922 pixels differ, a few glyphs only, each by +-1-2 (mostly green). Eve
|
||||
|
||||
The general fix would reset the whole object on reuse, or sort the ids that `delete_range` frees. Neither is needed for this symptom. **Confidence: [H]** for the cause and the fix (A/B reproduced, fixed, bisected to the one GL parameter).
|
||||
|
||||
## D448 — Split-screen pad seats keyed by SDL instance id (issue #99, D416 lineage, 2026-09-30)
|
||||
|
||||
**Status: PARTIAL.** Committed as `918ac5f9` on `feat/mp-splitscreen-99` (the commit message calls it "DNEW-mp-1"; this is its label). It was recovered from a crashed session's uncommitted worktree (archive `2026-09-30-recovery/mp-uncommitted-input-bg.patch`). Its TEMP-VPADSEAT probe was removed before commit.
|
||||
|
||||
**What:** `port/src/input.c` `pads[]` is indexed by *seat*, not by SDL device index, and each seat remembers its SDL joystick instance id. Outside a match, seats compact (the first attached pad is seat 0, and so on). During a match they stay reserved, so unplugging one pad no longer shifts the other players' seats or flips the Auto → keyboard/mouse-as-P1 routing. A rescan keeps pads that are still attached open instead of closing and reopening them. Port-only.
|
||||
|
||||
**Verified:** build, plus a 2P smoke (`GE_STARTMP=2 GE_MPVIRT=1`, 1280x720) in Window and Original aspect modes after merging `release/v0.4.1`. **Owed (maintainer, live):** plug/unplug pads mid-match (the seats must not shift), and check the keyboard/mouse-as-P1 routing is unchanged after an unplug. Related: outside PR #122 (hot-plug reopen, dolent) overlaps this area. Review it against this design before merging it.
|
||||
|
||||
## D454 — knife slash crash: keyframe pointer held in `u32` (ABI/layout exception, 2026-09-30)
|
||||
|
||||
@@ -16005,6 +16111,23 @@ The general fix would reset the whole object on reuse, or sort the ids that `del
|
||||
**By-eye test (maintainer):** AK47/Uzi held fire with `FpsCap` 60, 45 (or any unsteady load) and 30; empties the magazine in about the same wall time as 1964/GEPD at every setting; no stutter-bursts.
|
||||
**Probes left in tree:** none (env-gated `GE_D427`/`GE_D427_OLD` probes stripped).
|
||||
|
||||
## D450 — a replugged controller is never reopened: the render thread's event pump swallows the hot-plug events (2026-09-29)
|
||||
|
||||
**Status:** FIXED, port-only. User report (GameSir-G7 Pro): "disconnecting the controller, then reconnecting it did not seem to be picked up". The play log had only the startup `input: rescanned pads` line.
|
||||
|
||||
**Root cause.** Two threads drain the one SDL event queue:
|
||||
- `port/src/video.c` `videoPumpEvents`: `case SDL_CONTROLLERDEVICEADDED / REMOVED: inputRescanPads();`.
|
||||
- `port/fast3d/gfx_sdl2.cpp` `gfx_sdl_handle_events` (called from `gfx_run` on the render / scheduler thread every frame): **no case for them**. Whatever it dequeues is dropped.
|
||||
|
||||
Whichever loop runs `SDL_PollEvent` first gets the event, and the render loop usually wins (the same race WI-1 documented for ESC). So the pads were never rescanned and `pads[]` kept the closed controller's handle. Headless repro with a virtual pad (attach, detach, re-attach): one rescan at startup, none at the detach, and at the re-attach the process aborted with `malloc(): unaligned tcache chunk detected` (stale handle). A second latent problem: `inputRescanPads` ran on the host thread while the scheduler thread reads `pads[]` in `inputComputePad`; closing pads under a reader is a race.
|
||||
|
||||
**Fix.**
|
||||
- `gfx_sdl2.cpp`: new `case SDL_CONTROLLERDEVICEADDED / REMOVED: inputRequestRescan();`.
|
||||
- `video.c`: the case now calls `inputRequestRescan()` instead of `inputRescanPads()`.
|
||||
- `input.c`: `inputRequestRescan()` sets an atomic; `inputUpdate()` consumes it first thing and runs `inputRescanPads()` there, i.e. on the thread that is about to read the pads (the scheduler thread, or the game thread under tick input).
|
||||
|
||||
**Verified** (on the netplay line, headless, virtual pads through a test seam): attach -> detach -> attach -> detach -> attach with no tick input: 5 rescans (startup + one per event), no crash, against 1 rescan then a heap-corruption abort before the fix; with tick input the pad's buttons are seen before, after the first and after the second re-attach; solo `-level_09` frame 1800 byte-identical to the base. Real-pad retest by the user (GameSir-G7 Pro, 2026-09-29): passed.
|
||||
|
||||
|
||||
## D461 — `p_itemcur` allocated with the N64 stride; overran into the first prop model file (ABI/layout exception, 2026-09-30)
|
||||
|
||||
@@ -16028,3 +16151,34 @@ The general fix would reset the whole object on reuse, or sort the ids that `del
|
||||
**Found by.** The local-Qwen read-only audit of every `mempAlloc*` size expression (top suspects #1 and #2); the lead verified the literal at `vtxstore.c`, the C-stride indexing, and the D457(d) diff.
|
||||
|
||||
**Verified.** Build clean; solo `-level_09` to frame 420 and a 4-player `GE_STARTMP=4 GE_MPVIRT=3` run to frame 500 both exit rc=0 with no crash log. **Confidence: [H]** on the overrun, [M] on any visible effect (none expected). Remaining audit suspects: `size_item_buffer` weapon model buffers (`initBondDATA.c`), `model.c` rwdata word count vs converted data, `image_bank.c` +0x1000 slack: all UNKNOWN, need a converted-size check.
|
||||
|
||||
**Residual (pre-existing):** `inputRumble` can run on the game thread while a rescan runs on the scheduler thread in non-tick mode: one poll of exposure instead of an arbitrary host-thread moment.
|
||||
|
||||
|
||||
## D463 — zero-line tile texrect divided by zero in the texture importers (fast3d, 2026-10-01)
|
||||
|
||||
**Symptom.** The scripted split-screen sweep (`GE_STARTMP`, `GE_MPVIRT`, per-slot `GE_INPUTSCRIPT_P1..P3`) crashed once in 18 runs (3P, stage_id 48, Library) with `EXCEPTION 0xc0000094` = integer divide by zero (the sweep report's 'stack-buffer-overrun/fastfail' label was wrong; fastfail is 0xc0000409). A repro with `GE_D116=1` gave 2 crashes in 14 runs (3P and 4P, stage index 4), so it is not player-count specific.
|
||||
|
||||
**Cause.** Crash PC = `import_texture_i8` (`gfx_pc.cpp`, `height = size_bytes / rdp.texture_tile[tile].line_size_bytes`) with `line_size_bytes == 0`, reached via `gfx_dp_texture_rectangle` -> `gfx_draw_rectangle` -> `gfx_sp_tri1` -> `import_texture`. In both crashes the log shows, immediately before the fault, `[D116] O-glyph ... idx=0 w=0 loadw=0 h=0 pd=0000000000000000` (a zeroed font record drawn by `textRenderGlyphOutlined`, `src/game/textrelated.c`) followed by a zero-area `[D116/f3d]` texrect on a tile with `line_bytes=0`. `gDPLoadTextureBlock` with width 0 expands to `gsDPTile(line=0)` plus a zero-area `gSPTextureRectangle`; the N64 RDP rasterises nothing for it, so the decomp's output is faithful and fast3d must tolerate it. All the importers share the unguarded division (rgba16, ia8, i8, ci8, ...). **Not established:** which text produces the zeroed glyph (the outlined-text path reads `chars[*text - 0x21]`, so a character below 0x21 or an unpopulated font slot is the suspect). Rare because it depends on the string being drawn.
|
||||
|
||||
**Fix (port-layer, `port/fast3d/gfx_pc.cpp`, commit `81b57504`).** At the top of `import_texture`, if the tile's `line_size_bytes == 0`: log one capped line (`D463: texrect on zero-line tile -- import skipped`, at most 8 per run) and return without importing (no cache entry, no upload). The downstream triangle path already treats a zero line size as 1, and the degenerate draw rasterises no fragments. Nonzero line sizes take the original path unchanged. No game-logic change.
|
||||
|
||||
**Verified (local Qwen driver, lead re-read the logs).** After the fix: 8 repeats of the crashing 3P/stage-4 configuration, the full 18-run 2P/3P/4P matrix, solo `-level_09` and a menu boot all exit rc=0 with no crash log; the guard fired in 3 of the 8 repeats (so the condition recurs and is now harmless). 3P renders three viewports with an empty fourth quadrant (as on the N64) and 4P a clean 2x2. **Confidence: [H]** for the crash and the guard; [M] on the upstream glyph cause.
|
||||
|
||||
**Generalisable.** Any importer that divides by a tile field must tolerate 0; the game emits zero-sized loads legitimately. `0xc0000094` is a divide by zero, `0xc0000005` an access violation, `0xc0000409` a fastfail.
|
||||
|
||||
|
||||
|
||||
## D464 — GE_MEMPREDZONE: red-zone checker for the bump allocator (diagnostic tool, 2026-10-01)
|
||||
|
||||
**Falsifier (written before any run).** Re-introduce the D462 bug in a throwaway, uncommitted edit (`tmp = 0x14;` in `src/game/vtxstore.c` instead of `sizeof(struct unk_09B7A0_struct_parent)`), build, run `GE_MEMPREDZONE=1 GE_QUITFRAME=300 ./data/ge007.x86_64.exe -level_09`. Expected: at least one `MEMPREDZONE: HIT` whose `caller=` resolves (addr2line) to `vtxstore.c`'s allocation of the two parent tables, with `size` = count*0x14. If no hit names vtxstore, the checker is not trustworthy.
|
||||
|
||||
**What was built.** `src/memp.c` under `#ifdef PORT`, gated by `GE_ENVFLAG("GE_MEMPREDZONE")` (cached; one branch per allocation when unset, the original allocator body is kept verbatim as `mempAllocBytesInBankRaw`). When set, every allocation with room is padded by a 64-byte trailer (pattern `0xA5 + 13*i`), recorded in an 8192-entry side table (block, size, trailer, id, caller = `__builtin_return_address(0)` of `mempAllocBytesInBank` / `mempAllocPackedBytesInBank`, bank). Alignment is unchanged (pos advances 64 more). `mempRedzoneCheck(why)` logs one `[ERROR] MEMPREDZONE: HIT id= bank= size= block= rz_first_off= changed=n/64 caller=<raw addr>` per damaged entry and one `[INFO]` summary per check. Check points: `mempResetBank`, `mempNullNextEntryInBank` (level unload; the bank's entries are dropped afterwards), `videoEndFrame` every 60th frame, and the orderly-quit path (`videoHostExitIfRequested`, render parked). Resolve a caller with `addr2line -f -C -i -e build-pc/ge007.x86_64.exe 0x<addr>` (PE image base 0x140000000 is already in the logged address). Covers all banks (the bank is derived from the block address). No `src/game` edit.
|
||||
|
||||
**Bump-pointer users audited.** `mempAddEntryOfSizeToBank` (callers: `fileIndexLoadToBank`, `fileSetSize` in ob.c) shrinks the *last* allocation, and `fileIndexLoadToBank` allocates the whole remaining bank (`mempGetBankSizeLeft`) then shrinks. A trailer cannot fit on the whole-bank allocation, so that block is left unpadded (`untracked(no room)` in the summary) and gains its trailer when the shrink leaves room; `mempAddEntryOfSizeToBank` re-places the trailer for tracked blocks (`newsize + 64` accounting). Nothing else computes sizes from pointer differences or reads `prevpos` (grep: only memp.c). `bg.c:612` only logs pool pointers. No bank had to be excluded.
|
||||
|
||||
**Falsifier result: PASS.** With `tmp = 0x14` temporarily restored in `vtxstore.c` (uncommitted; reverted, `git diff -- src` empty afterwards), `GE_MEMPREDZONE=1 GE_QUITFRAME=300 -level_09` reported `HIT id=21 bank=4 size=400 ... caller=0x1400d7c85` and `HIT id=23 size=800 ... caller=0x1400d7cbc`; addr2line resolved both to `sub_GAME_7F09B820`, `src/game/vtxstore.c:117` and `:119` (the two table allocations). So the checker catches the D462 class on a normal solo load. (Hit trailer damage was only 6/64 bytes: the overrun writes -1/0 into a few fields, so a pattern-equal byte can hide, but any real overrun beyond the first stride row hits.)
|
||||
|
||||
**Sweep (build with the D462 fix restored, flag set, 37 launches).** All 21 solo levels (`-level_XX`, 420 frames; names/numbers from `kSoloLevels`): rc=0, no `ge007.crash.log`, **0 red-zone hits**, tracked blocks 126 (Cuba) to 788 (Jungle), 19..59 untracked-no-room. Menu boot (900 frames): rc=0, 0 hits. **The MP matrix did not exercise multiplayer:** `GE_STARTMP`/`GE_MPVIRT` exist only on `feat/mp-splitscreen-99` (`src/game/lv.c`), not on `release/v0.4.1`, so those 15 runs were menu boots (identical 84-tracked-block logs). The split-screen branch is too far behind (pre-D453, conflicts in memp.h) to cherry-pick onto; the launch cap (40) was already nearly spent. **Owed:** rebuild the checker onto the split-screen branch and run the 2P/3P/4P matrix there (the D461 victim was an MP-only `equipmaxitems`-sized path, and MP stage allocations differ).
|
||||
|
||||
**Verdict.** On the solo levels and the front end, no remaining allocation overrun is visible at the 64-byte granularity, including after D461/D462. Not covered: overruns landing in a block's *interior* neighbour that never reaches the trailer (stride error smaller than the trailer is caught; one far beyond 64 bytes skips the trailer and corrupts the next block's data, but still damages the next block's own trailer only if it reaches it), writes into the unpadded whole-bank allocations, blocks beyond the 8192-entry table (none reached it), under-runs (no leading zone). **Confidence: [H]** that the tool detects the D462 class; [M] that solo levels are clean (420 frames only, load-time allocations; runtime allocations after frame 420 not covered).
|
||||
Reference in new issue
Block a user