diff --git a/docs/dev/findings-index.csv b/docs/dev/findings-index.csv index 87dd080e..369e526b 100644 --- a/docs/dev/findings-index.csv +++ b/docs/dev/findings-index.csv @@ -165,7 +165,7 @@ D258,OPEN,PAL/JP asset conversion is broken at the source-data level —,"OPEN D284,FIXED,port/src/random.c's MIPS64 dsll32/dsrl32 shift helpers were backwards —,"FIXED, 21-level sweep clean, no crashes —" D285,PARTIAL,Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug:,"PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18)." D286,OPEN,"User-confirmed real ~8s frame freeze on Caverns, Steam Deck —","OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285." -D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; gfxFrameMsgQ full is a lead, not a confirmed cause." +D287,OPEN,"User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the… —","FIX APPLIED, pending user re-test (2026-09-25) —" D288,CLOSED,"Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… —","CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone (""silo fixed"")." D289,OPEN,"Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).","OPEN — needs a human re-test specifically on a build with the D255 fix (this session's release/v0.3.0, not any earlier build) to see if it still happens.…" D290,FIXED,"Watch's in-game ""controller"" section shows no control-pad graphic (M-141, Steam Deck user playtest).","FIXED (2026-09-23): D264-pattern template read in watchRenderController (flags read 0), plus 3 pointer-width/raw-Gfx sites on the same path; controller model verified rendering on Windows." diff --git a/docs/dev/findings.md b/docs/dev/findings.md index 8c1fbbc7..5aa48356 100644 --- a/docs/dev/findings.md +++ b/docs/dev/findings.md @@ -573,7 +573,7 @@ covers D24–D69; the log continues in §H (D32 procedure, D70–D121). | D284 | **`port/src/random.c`'s MIPS64 `dsll32`/`dsrl32` shift helpers were backwards — PC PRNG desynced from the N64's from draw 1, in every release to date (M-138 review; fix M-139b).** — full `## D284` entry at file tail | FIXED, 21-level sweep clean, no crashes — RNG stream changes from every prior build; existing replays will diverge. **"Savegames unaffected" was FALSE (M-148): `fileGenerateCRC` runs off a local constant seed via `randomGetNextFrom`, so the fix invalidates every stored slot CRC — v0.3.0 silently wipes all pre-fix saves on first launch (release-blocker candidate, see D295 M-148)** | | D285 | **Steam Deck SIGSEGV during D255 live-testing = SEPARATE bug: the `sndHandleEvent` preempt-scan walks the active-sound list unlocked (M-140; fixed M-141).** — full `## D285` entry at file tail | **PARTIALLY FIXED, RECURRED PAST THE FIX (see D305, 2026-09-18).** `osSetIntMask` lock added around the preempt-scan (M-141), live-verified crash-free on real Deck hardware (2026-09-16) — but the exact same fault (same FAULT ADDR, same site) recurred on the v0.3.0 pre-release build, disproving the lock as a *complete* fix: it stops concurrent mutation but not a scan that starts on an already-empty list. D305 adds the actually-missing NULL guard. Do not conflate with D255 — different thread, different subsystem. | | D286 | **User-confirmed real ~8s frame freeze on Caverns, Steam Deck — self-recovered, not a crash (M-140).** — full `## D286` entry at file tail | OPEN — one confirmed occurrence, no repro trigger identified, not root-caused; distinct from D255/D285. Do not chase a repro without a better lead. | -| D287 | **User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the…** — full `## D287` entry at file tail | OPEN — one confirmed occurrence, real (not self-recovering), not root-caused; `gfxFrameMsgQ` full is a lead, not a confirmed cause. | +| D287 | **User-confirmed real, non-self-recovering freeze on Windows (M-141, 2026-09-15/16) after ~15-20 minutes with no input, triggered by clicking back into the…** — full `## D287` entry at file tail | FIX APPLIED, pending user re-test (2026-09-25) — likely mechanism found by code review: SDL mouse-mode/cursor calls (`SDL_SetRelativeMouseMode`, `SDL_ShowCursor`) were made from the scheduler thread (every controller poll) concurrently with the host (window) thread's focus/click handling; the idle+refocus trigger is exactly when both flip grab state. Now queued and applied only on the host thread (`inputApplyMouseRequests`). Not reproduced; confirm with an idle 20 min + refocus test. | | D288 | **Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —…** — full `## D288` entry at file tail | CLOSED — not reproduced on the current build (2026-09-23 capture) and the 2026-09-25 user Silo playtest confirms it gone ("silo fixed"). Fixer not isolated (D271 A/B ruled out). | | D289 | **Dam: a crash in the computer room, not reproducible on a second attempt (M-141, Steam Deck user playtest).** — full `## D289` entry at file tail | OPEN — needs a human re-test specifically on a build with the D255 fix (this session's `release/v0.3.0`, not any earlier build) to see if it still happens.… | | D290 | **Watch's in-game "controller" section shows no control-pad graphic (M-141, Steam Deck user playtest).** — full `## D290` entry at file tail | FIXED (2026-09-23): D264-pattern template read in `watchRenderController` (flags read 0), plus 3 pointer-width/raw-Gfx sites on the same path; controller model verified rendering on Windows. | @@ -11734,6 +11734,13 @@ Superseded history below (the original OPEN investigation, root cause not found **Status (from §F table):** OPEN — the `d287_freeze_gdb.txt` file this entry cited as evidence turned out to be a normal-exit capture, not a freeze capture (M-152 correction); no real freeze-time thread state has been analyzed yet. `gfxFrameMsgQ` full remains an architecturally-plausible lead (traced statically: consumer-side stall via an unbounded `pthread_cond_wait` in `rsp.c:271`'s `osSendMesg`, if the scheduler's `cmdQ` fills), not a confirmed cause. Needs a fresh live capture — ideally with a raised/env-gated heartbeat-report cap (see above) plus `GE_D60` — to get mainThread's actual backtrace at freeze time. +**2026-09-25: likely mechanism (code review) + fix applied, pending a re-test.** + +- **Queue-deadlock leads ruled out statically.** The only blocking sends are `rsp.c:271` (main → sched `cmdQ`, 8 slots), `sched.c:428` (task-done → `gfxFrameMsgQ`) and PI completions. The main loop caps work in flight at `pendingGfx < 2`, so it can never fill `cmdQ` and block there; a sched↔main circular wait cannot close. The shared-condvar `pthread_cond_signal` pattern in `osSendMesg`/`osRecvMesg` needs two blocking waiters of the same kind on one queue plus an opposite waiter; no queue in the game has that shape. `joyPoll`'s blocking receive (joy.c:438) is balanced by `osContStartReadData`'s immediate completion post. +- **What does fit "idle, then click back into the window":** `port/src/input.c` called `SDL_SetRelativeMouseMode` and `SDL_ShowCursor` from **two threads**. The scheduler thread did so on every controller poll (`osContStartReadData` → `contSnapshotFromKeyboard` → `inputComputePad` → `reconcileGrab`/`applyCursorVisibility`); the host thread, which created the window and pumps its events (`port/src/main.c` loop → `videoPumpEvents`), did so from focus/click handling (`inputSetMouseGrab`, `inputNotifyClick`). SDL's mouse/window API is not thread-safe and belongs on the window's thread. On Windows, relative mode starts/stops SDL 2.30+'s raw-input thread and re-clips the cursor. Refocus + click is precisely when both threads flip the grab state at the same moment. The port already had this rule for F10 window changes (`videoDrainWindowRequests`), but mouse mode was never routed that way. +- **Fix (port-only, `port/src/input.c`, `port/src/video.c`, `port/include/input.h`):** `applyGrab`, `applyCursorVisibility` and `inputSuspendForOverlay` still update the logical state immediately, but only *record* the wanted relative-mode/cursor state (spinlock, last writer wins). `inputApplyMouseRequests()` applies it, deduplicated, on the host thread at the start and end of every `videoPumpEvents()` (and once in `inputInit`, which runs on the host thread). `inputUpdate` skips relative deltas until relative mode is actually on, so the ≤8 ms gap reads no absolute motion. +- **Verified:** Windows + Linux builds clean; golden gate 3/3. **Not verified:** the freeze itself (needs the idle 15–20 min + refocus repro) and mouse feel. **User checks:** click-to-lock, mouse-look, ESC release, alt-tab away/back, F10 overlay cursor, cursor hidden in menus; then leave the window unfocused ~20 min and click back in. + ## D288 — Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report —… **Silo intro: a huge, flat-shaded, untextured brown/tan triangle sweeps across the frame during the camera pan down the missile shaft (M-141, user report — "weird triangle drawn on silo intro randomly when the camera pans around the rocket"; previously catalogued as `GRAPHICS-BACKLOG.md` D171, "observed, not investigated" since M-34, never given a full findings.md entry until now).** Reproduced headlessly this session: `GE_PCDUMP=1-2000:10 ./build-pc/ge007.x86_64.exe -level_20` (run backgrounded, not foregrounded — see the environment note below), captured to `./ppm/`. Visual sweep (contact-sheet montages, `docs/img/bugs/D288-silo-stray-triangle-sheet.png` covers frames 571-751; `-closeup.png` is a full-res crop of frame 691) shows: **clean at frames 481-561** (no triangle), **first appears between frame ~562-601**, grows continuously in screen size through frame ~711 (by which point it covers roughly a third of the frame), then **shrinks and is gone by ~731-741** as the scene cuts to the first-person shaft-corridor view. Sampled pixel color is **exactly uniform** across the whole triangle (`RGB(83,62,44)` / `#533E2C` at two different sample points) — no gradient, no texture detail — i.e. this is a flat-shaded (no per-vertex color variance, no texture bound) single triangle, not garbage/noisy vertex-color data. **Reading of the evidence:** the grow-then-shrink-then-vanish arc across ~15 real seconds of camera motion is the classic signature of a genuine 3D triangle in world space that the camera flies close to/through during the scripted pan — not a 2D HUD/UI element (which would stay roughly fixed in screen size) and not obviously a `D146`-class freed-memory read (which reads as noisy garbage bytes, not a clean uniform flat color). The flat uniform color argues this is a REAL, correctly-colored asset triangle whose position/transform is wrong, which points at the same neighborhood as D106/D233 (near-plane / w-clip mishandling in `port/fast3d/gfx_pc.cpp`) rather than a game-code or converter data bug — but this is a read of the visual evidence, not a code-level confirmation; no probe has been run yet. **Not yet identified:** which model/prop this triangle belongs to (nothing in `UsetupsiloZ.c` was grepped for a matching solid-brown material this session), and no `port/fast3d/` tracing was done to catch the actual triangle at draw time. **Environment note (new, useful beyond this bug):** launching `ge007.x86_64.exe` in the **foreground** under this session's Bash tool with `timeout N ...` produced **zero** `ppm/` output despite `GE_PCDUMP` being set and verified exported — the `ppm/` directory was never even created (`GE_MKDIR` never ran or `frames`-gated capture never fired). Backgrounding the same invocation (`... &`, `sleep N`, `kill`) worked immediately and reliably. Not root-caused (didn't block this investigation), but worth remembering for the next session that needs `GE_PCDUMP` from this same tool. **Next, concrete:** (1) grep `assets/obseg/setup/UsetupsiloZ.c` / the missile-shaft model's material table for a solid `RGB(83,62,44)`-ish prim/vertex color to identify the source asset; (2) add a temporary `#ifdef PORT` probe in `gfx_sp_tri1` logging any triangle whose post-transform screen-space bounding box exceeds some fraction of the viewport together with each vertex's `w`, gated to `-level_20` frames ~560-740, to catch the culprit triangle's `w` sign/magnitude at the moment it blows up — same style as the existing `GE_D271`/`GE_D104` probes; (3) cross-check against D233's `w<0` trivial-reject fix and D106/D271's near-plane portal-bound clamp to see if this is a third site with the same underlying class of bug, or a genuinely new mechanism. diff --git a/port/include/input.h b/port/include/input.h index 27fa4ab2..3104c4bc 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -43,6 +43,8 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y); * also suspends mouse-aim reads until re-grabbed. No-op if the mouse is * disabled in config. */ void inputSetMouseGrab(int on); +/* D287: apply queued mouse-mode/cursor changes; host (window) thread only. */ +void inputApplyMouseRequests(void); /* WI-1 click-to-lock cursor capture. The host event pump calls * inputNotifyClick() when a mouse button goes down inside the game window diff --git a/port/src/input.c b/port/src/input.c index de03083b..c7de784d 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -281,6 +281,55 @@ static int padSelectPrev = 0; /* Select (BACK) edge: overlay toggle */ static int mouseEnabled = 1; static int mouseGrabbed = 1; /* released while the window is unfocused */ +/* D287: SDL's mouse-mode and cursor calls belong on the window's thread (the + * host thread that created it and pumps its events). Controller polls run on + * the scheduler thread (osContStartReadData -> inputComputePad -> + * reconcileGrab), so they called SDL_SetRelativeMouseMode / SDL_ShowCursor + * concurrently with the host thread's own focus/click handling. On Windows + * relative mode starts/stops SDL's raw-input thread and re-clips the cursor; + * two threads flipping it at once (clicking back into the window after being + * unfocused) can wedge. Callers now only record the wanted state; the host + * thread applies it in inputApplyMouseRequests() from videoPumpEvents(). The + * logical state (mouseGrabbed, deltas) still updates immediately. */ +static SDL_SpinLock s_mouseReqLock; +static int s_reqRelative = -1, s_reqCursor = -1; /* -1: nothing pending */ +static int s_appRelative = -1, s_appCursor = -1; /* host thread only */ + +static void mouseRequestRelative(int on) +{ + SDL_AtomicLock(&s_mouseReqLock); + s_reqRelative = on ? 1 : 0; + SDL_AtomicUnlock(&s_mouseReqLock); +} + +static void mouseRequestCursor(int show) +{ + SDL_AtomicLock(&s_mouseReqLock); + s_reqCursor = show ? 1 : 0; + SDL_AtomicUnlock(&s_mouseReqLock); +} + +/* Host thread only (videoPumpEvents, inputInit). */ +void inputApplyMouseRequests(void) +{ + SDL_AtomicLock(&s_mouseReqLock); + const int rel = s_reqRelative, cur = s_reqCursor; + s_reqRelative = s_reqCursor = -1; + SDL_AtomicUnlock(&s_mouseReqLock); + + if (rel >= 0 && rel != s_appRelative) { + s_appRelative = rel; + SDL_SetRelativeMouseMode(rel ? SDL_TRUE : SDL_FALSE); + if (rel) { + SDL_GetRelativeMouseState(NULL, NULL); /* drain the accumulated jump */ + } + } + if (cur >= 0 && cur != s_appCursor) { + s_appCursor = cur; + SDL_ShowCursor(cur ? SDL_ENABLE : SDL_DISABLE); + } +} + /* WI-1: Quake-style click-to-lock cursor capture. The cursor is free until * you click in the game window; ESC (or focus loss, or opening a menu) frees * it again. Re-entering a stage while still "armed" re-locks automatically so @@ -712,12 +761,11 @@ int inputInit(void) SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); sysLogPrintf(LOG_INFO, "input: raw mouse input (no OS pointer accel)"); } - SDL_SetRelativeMouseMode(mouseGrabbed ? SDL_TRUE : SDL_FALSE); - /* Drain the initial jump. */ - SDL_GetRelativeMouseState(NULL, NULL); + mouseRequestRelative(mouseGrabbed); } applyCursorVisibility(); /* hide the OS cursor if we start focused */ + inputApplyMouseRequests(); /* inputInit runs on the host thread */ sysLogPrintf(LOG_INFO, "input: ready (mask=0x%x, %d controller(s), aimSpeed=%d)", connectedMask, numControllers, mouseAimSpeed); @@ -743,7 +791,10 @@ void inputUpdate(void) { SDL_GameControllerUpdate(); - if (!mouseEnabled || !mouseGrabbed) { + /* D287: skip until the host thread has actually enabled relative mode + * (a benign racy read of a host-thread int), so the few ms between the + * logical grab and its application do not read absolute cursor motion. */ + if (!mouseEnabled || !mouseGrabbed || s_appRelative != 1) { return; } @@ -1448,10 +1499,7 @@ static void applyGrab(int want) return; } mouseGrabbed = want; - SDL_SetRelativeMouseMode(want ? SDL_TRUE : SDL_FALSE); - if (want) { - SDL_GetRelativeMouseState(NULL, NULL); /* drain the accumulated jump */ - } + mouseRequestRelative(want); /* D287: applied on the host thread */ mouseDX = mouseDY = 0.0; } @@ -1477,7 +1525,7 @@ static void applyCursorVisibility(void) /* D194: while an aim hold has the cursor out for absolute aim, show it -- * the user is pointing with it and needs to see where. */ int hide = windowFocused && mouseEnabled && !s_absAimSuspend; - SDL_ShowCursor(hide ? SDL_DISABLE : SDL_ENABLE); + mouseRequestCursor(!hide); /* D287: applied on the host thread */ } /* video.c focus events. Records focus and lets reconcileGrab() decide. */ @@ -1531,10 +1579,10 @@ void inputSuspendForOverlay(void) { if (mouseGrabbed) { mouseGrabbed = 0; - SDL_SetRelativeMouseMode(SDL_FALSE); + mouseRequestRelative(0); mouseDX = mouseDY = 0.0; } - SDL_ShowCursor(SDL_ENABLE); + mouseRequestCursor(1); } void inputPostWheel(int notches) diff --git a/port/src/video.c b/port/src/video.c index cbc5630d..788da0ba 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -646,6 +646,7 @@ void videoPumpEvents(void) /* Apply any window/fullscreen change the F10 overlay posted from the * scheduler thread (must run here, on the window's creating thread). */ videoDrainWindowRequests(); + inputApplyMouseRequests(); /* D287: mouse mode/cursor, same rule */ SDL_Event ev; while (SDL_PollEvent(&ev)) { @@ -726,6 +727,9 @@ void videoPumpEvents(void) wmAPI->set_window_title(title); } } + + /* D287: apply anything the events above (click-to-lock, focus) queued. */ + inputApplyMouseRequests(); } void videoSubmitCommands(Gfx *cmds)