diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 73ffce48..9b78b5d2 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -311,6 +311,27 @@ behavior exactly. idle machine the user drove Silo live past ~1800 frames, VI pacemaker healthy, no stall. See `docs/M-25-QOL-REVIEW.md`. +## Done this session (M-26) — port-layer QoL batch (A/B/C/E) + +Low-risk QoL, `port/` only, zero `src/` change. All defaults reproduce +prior behaviour. Full review: **`docs/M-26-QOL-REVIEW.md`**. + +- **A — save-on-exit + `[Window]` persistence.** `atexit(portAtExit)` in + `main.c` (`videoSaveWindowState()` + `configSave()`), fires on every + clean quit (`exit(0)` in `videoPumpEvents`); `abort()` paths skip it. + New `[Window]` ini section (`Width/Height/X/Y/Maximized`, sentinels = + old behaviour) in `video.c`; `Video.Fullscreen` now round-trips. +- **B — `level_sweep.sh` STALLED verdict.** Frames rendered then froze + with the process still alive → `STALLED (froze at N/M frames)` instead + of a bogus PASS (the Silo case). Retried once like NO-FRAMES. +- **C — raw mouse input.** `Input.MouseRawInput=1` (default 0) sets the + SDL relative-scale / warp hints off in `inputInit` — no OS pointer accel. +- **E — `--help` / `--version`** in `main.c` before `crashInit()`: build + id, usage, the 21-level `-level_XX` table. +- **Verified:** build green (`ntsc-final`); `-level_09` + `-level_20` + boot crash-free 6/6 GE_PCDUMP frames (unregressed); `ge007.ini` gains a + populated `[Window]` block on exit. + ## Next task (M-24 — Opus 5) **Scope call (user, M-23):** D139 (stage unload) + D140 (pause menu / watch) diff --git a/docs/M-26-QOL-REVIEW.md b/docs/M-26-QOL-REVIEW.md new file mode 100644 index 00000000..fcb0e03b --- /dev/null +++ b/docs/M-26-QOL-REVIEW.md @@ -0,0 +1,56 @@ +# M-26 QoL review — save-on-exit, window persistence, raw mouse, --help, STALLED verdict + +Port-layer only. Zero `src/` / game-logic change. Every default reproduces +prior behaviour exactly (all new knobs default-off / default-current). + +## A — `atexit()` save-on-exit + `[Window]` persistence + +- `port/src/main.c`: `atexit(portAtExit)` registered right after + `configLoad()`. `portAtExit` = `videoSaveWindowState()` then + `configSave()`. The `exit(0)` in `videoPumpEvents` (SDL_QUIT / ESC / + window-close) is reachable, so the handler fires on every clean quit. + Crash / `sysFatalError` paths call `abort()` → handler does **not** run + (intended — don't persist a broken state). +- `port/src/video.c`: new `[Window]` config section — `Width`, `Height`, + `X`, `Y`, `Maximized`. Sentinels reproduce the old hardcoded behaviour: + `Width/Height = 0` → native (640×480 / 640×400), `X/Y = -1` → SDL + centres the window. `videoInit` uses them for the `GfxWindowInitSettings`. +- `videoSaveWindowState()` reads `wmAPI->get_fullscreen_state` / + `get_maximized_state` / `get_dimensions` (all already in the SDL2 wapi + backend) and writes geometry back into the cfg vars. A maximized or + fullscreen window keeps its last *restored* size/pos on disk and only + updates the flag. `Video.Fullscreen` now round-trips too. +- **Verified:** `-level_09` / `-level_20` boot crash-free (6/6 GE_PCDUMP + frames each, unregressed); `ge007.ini` gains a populated `[Window]` + block on exit; relaunch reads it back with no unknown-key warnings. + +## B — `level_sweep.sh` STALLED verdict + +Pure shell (not built). A level that renders a few frames then freezes +while the process stays alive (the Silo `-level_20` fly-down case, +D117/D134 machine-load sensitivity) previously reported **PASS** because +`last=$(ls ppm/*.ppm | tail -1)` was non-empty. Now the watchdog records +`stalled=1` when the frame count is frozen for `STALL_SECS`, and `alive=1` +if the process was still running when the watchdog gave up → +`STALLED (froze at N/M frames)`. A complete dump set is still a real PASS. +STALLED is retried once (like NO-FRAMES) before it's believed. + +## C — raw mouse input (`Input.MouseRawInput`, default 0) + +`port/src/input.c inputInit`: when `MouseRawInput=1`, sets +`SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE=0` and +`SDL_HINT_MOUSE_RELATIVE_MODE_WARP=0` before `SDL_SetRelativeMouseMode` — +removes OS pointer acceleration / warp emulation from aim. Off by default +so nothing changes unless the user opts in. + +## E — `--help` / `--version` + +`port/src/main.c`, handled before `crashInit()`: +- `--version` → rom id / target platform / build hash, exit 0. +- `--help` / `-h` → the above + usage + the 21 solo-level `-level_XX` + table (matches `tools_pc/level_sweep.sh` / `playtest.sh --list`), exit 0. + +## Not done (Tier 2/3 from the board) + +F (minidump), G (PNG screenshots), H (rumble), I/J (in-render overlay / +toasts) — left for a dedicated session. diff --git a/port/src/input.c b/port/src/input.c index c6d1568e..71402e8f 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -129,6 +129,7 @@ static int mouseTurnSpeed = 100; /* hipfire yaw sensitivity, percent */ static int mouseInvertY = 0; /* 1 = mouse-down looks up */ static int mouseYScale = 100; /* extra vertical (pitch) sensitivity, % */ static int mouseSmoothing = 0; /* 0 = raw; 1..90 = low-pass strength (%) */ +static int mouseRawInput = 0; /* 1 = bypass OS pointer accel for aim */ /* Gamepad tuning -- defaults reproduce the old hardcoded constants exactly. */ static int padDeadzone = STICK_DEADZONE; /* left-stick deadzone, raw 0..32767 */ @@ -202,6 +203,14 @@ int inputInit(void) /* Relative mouse mode for mouse-look. ESC still quits (video.c). */ mouseGrabbed = mouseEnabled; if (mouseEnabled) { + if (mouseRawInput) { + /* Feed the raw device delta straight through: no OS pointer + * acceleration, no warp-based emulation. Must be set before + * relative mode is enabled. */ + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_SYSTEM_SCALE, "0"); + SDL_SetHint(SDL_HINT_MOUSE_RELATIVE_MODE_WARP, "0"); + sysLogPrintf(LOG_INFO, "input: raw mouse input (no OS pointer accel)"); + } SDL_SetRelativeMouseMode(SDL_TRUE); /* Drain the initial jump. */ SDL_GetRelativeMouseState(NULL, NULL); @@ -477,6 +486,7 @@ PD_CONSTRUCTOR static void inputConfigInit(void) configRegisterInt("Input.MouseInvertY", &mouseInvertY, 0, 1); configRegisterInt("Input.MouseYScale", &mouseYScale, 1, 500); configRegisterInt("Input.MouseSmoothing", &mouseSmoothing, 0, 90); + configRegisterInt("Input.MouseRawInput", &mouseRawInput, 0, 1); configRegisterInt("Input.PadDeadzone", &padDeadzone, 0, 30000); configRegisterInt("Input.PadTriggerPct", &padTriggerPct, 1, 99); configRegisterInt("Input.PadLookInvertY", &padLookInvertY, 0, 1); diff --git a/tools_pc/level_sweep.sh b/tools_pc/level_sweep.sh index 235dce17..7a7ff090 100644 --- a/tools_pc/level_sweep.sh +++ b/tools_pc/level_sweep.sh @@ -66,18 +66,22 @@ run_level() { # known -- exit as soon as the whole dump set is on disk (PASS), or the frame # count has been frozen for STALL_SECS (level rendered a bit then wedged), or # the process exits on its own (clean exit / crash). - local waited=0 fcnt=0 prev=0 stall=0 + local waited=0 fcnt=0 prev=0 stall=0 stalled=0 while [ $waited -lt $SWEEP_SECS ] && kill -0 $pid 2>/dev/null; do sleep 1; waited=$((waited + 1)) fcnt=$(ls ppm/*.ppm 2>/dev/null | wc -l) [ "$fcnt" -ge "$EXPECT_FRAMES" ] && break if [ "$fcnt" -gt 0 ] && [ "$fcnt" -eq "$prev" ]; then - stall=$((stall + 1)); [ $stall -ge $STALL_SECS ] && break + stall=$((stall + 1)) + if [ $stall -ge $STALL_SECS ]; then stalled=1; break; fi else stall=0 fi prev=$fcnt done + # was the process still alive when we gave up (vs. a clean exit / crash)? + local alive=0 + kill -0 $pid 2>/dev/null && alive=1 taskkill //F //IM ge007.x86_64.exe > /dev/null 2>&1 wait $pid 2>/dev/null sleep 2 @@ -95,7 +99,14 @@ run_level() { last=$(ls ppm/*.ppm 2>/dev/null | tail -1) if [ -n "$last" ]; then pcnt=$(python tools_pc/pixcount.py "$last" 2>/dev/null | grep -oE 'non-clear [0-9]+ \([0-9.]+%\)') - echo "PASS $pcnt" + # frames rendered then froze while the process stayed alive -> STALLED, + # not a real PASS (the Silo -level_20 fly-down case, D117/D134 load + # sensitivity). A complete dump set is still a genuine PASS. + if [ "$stalled" = 1 ] && [ "$alive" = 1 ] && [ "$fcnt" -lt "$EXPECT_FRAMES" ]; then + echo "STALLED (froze at $fcnt/$EXPECT_FRAMES frames) $pcnt" + else + echo "PASS $pcnt" + fi else echo "NO-FRAMES" fi @@ -105,18 +116,19 @@ for entry in $LV; do name=${entry%%:*}; num=${entry##*:} status=$(run_level "$name" "$num") - # NO-FRAMES is the flaky outcome (D117 + host load) -- retry once before - # believing it. A CRASH is deterministic enough to report as-is. - if [ "$status" = "NO-FRAMES" ]; then - sleep 3 - retry=$(run_level "$name" "$num") - if [ "$retry" = "NO-FRAMES" ]; then - status="NO-FRAMES (2 attempts)" - else - status="$retry [passed on retry]" - [ -f $LOGS/$name.crash.log ] || true - fi - fi + # NO-FRAMES and STALLED are the flaky outcomes (D117 + host load) -- retry + # once before believing them. A CRASH is deterministic enough to report as-is. + case "$status" in + NO-FRAMES|STALLED*) + sleep 3 + retry=$(run_level "$name" "$num") + case "$retry" in + NO-FRAMES) status="NO-FRAMES (2 attempts)" ;; + STALLED*) status="$retry (2 attempts)" ;; + *) status="$retry [passed on retry]" ;; + esac + ;; + esac echo "$name ($num): $status" | tee -a $OUT done