From 46fa579e542db2c5e73a954969807075785ecb58 Mon Sep 17 00:00:00 2001 From: James Dansereau <106631954+jkdansereau@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:08:22 -0400 Subject: [PATCH] =?UTF-8?q?M-24:=20playtest=20QoL=20=E2=80=94=20F12=20scre?= =?UTF-8?q?enshot=20+=20mouse=20frees=20on=20focus=20loss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit port/src/video.c: F12 sets a request flag consumed in videoEndFrame (render thread, GL context current) -> ppm/shot_NNN.ppm via the existing gfx_opengl_dump_bound_fbo. SDL_WINDOWEVENT_FOCUS_LOST/GAINED now call inputSetMouseGrab(0/1) so alt-tabbing away releases the trapped cursor. port/src/input.c: add inputSetMouseGrab() — toggles SDL relative-mouse mode (respecting MouseEnabled), zeroes the aim delta, and gates inputUpdate mouse reads on a mouseGrabbed flag. -level_09 3/3 runs crash-free to frame 900, unregressed (91.6%). --- docs/HANDOFF.md | 9 ++++++++- docs/LEVEL-PLAYTEST.md | 12 ++++++++---- port/include/input.h | 6 ++++++ port/src/input.c | 18 +++++++++++++++++- port/src/video.c | 25 +++++++++++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index b587c892..4bcf4bd1 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -261,9 +261,16 @@ Pre-playtest QoL, port-layer only, no `src/` / game-logic change. - Verified: `-level_09` boots crash-free to 900+ frames @ 91.65% coverage (unregressed); `-level_20` crash-free; `ge007.ini` written then re-read with no unknown-key warnings. Build green (`ntsc-final`). +- **`port/src/video.c` QoL:** **F12** takes a screenshot + (`ppm/shot_NNN.ppm`, dumped on the render thread via the existing + `gfx_opengl_dump_bound_fbo`); window **focus loss frees the mouse** + (`inputSetMouseGrab(0)` on `SDL_WINDOWEVENT_FOCUS_LOST`, re-grab on + gain) so alt-tab works. `inputSetMouseGrab()` also zeroes the aim delta + and suspends mouse reads while released. +- Alt-Enter fullscreen toggle already existed (`gfx_sdl2.cpp:299`). - **Still TODO (not started):** key rebinding, gamepad hotplug (`SDL_CONTROLLERDEVICEADDED/REMOVED`), mouse-wheel weapon cycle, - screenshot/mouse-release hotkeys, on-screen FPS overlay. + on-screen FPS overlay, window-title FPS. ## Next task (M-24 — Opus 5) diff --git a/docs/LEVEL-PLAYTEST.md b/docs/LEVEL-PLAYTEST.md index 60ab4ff4..20c0d3b3 100644 --- a/docs/LEVEL-PLAYTEST.md +++ b/docs/LEVEL-PLAYTEST.md @@ -12,10 +12,14 @@ export PATH="/c/msys64/mingw64/bin:$PATH" && ./build-pc.sh ntsc-final ./build-pc/ge007.x86_64.exe -level_XX # bare — pools auto-injected (D121) ``` -Controls: `port/src/input.c` — keyboard+mouse or SDL gamepad. Known input -polish gaps (do not re-log): D118a mouse yaw slower than pitch, D118b -mouse-Y may feel inverted. Difficulty for a bare `-level_XX` boot defaults -to Agent; use the menu path (mission select → difficulty) to test 00/007. +Controls: `port/src/input.c` — keyboard+mouse or SDL gamepad. Mouse-look is +mode-aware since M-24 (hold RMB = analog aim). Tune feel in +`data/ge007.ini` `[Input]` (`MouseAimSpeed` 50, `MouseTurnSpeed` 100, +`MouseInvertY` 0) with the game closed. Known residual (do not re-log): +D118a — hipfire pitch is digital vs analog yaw. **F12** = screenshot to +`ppm/shot_NNN.ppm`. Alt-tab away frees the cursor (re-grabs on focus). +Difficulty for a bare `-level_XX` boot defaults to Agent; use the menu path +(mission select → difficulty) to test 00/007. Objective status: `src/game/objective_status.c` (`get_status_of_objective()` `:161`); objective propDefs are diff --git a/port/include/input.h b/port/include/input.h index b37c5caf..1ebc53a9 100644 --- a/port/include/input.h +++ b/port/include/input.h @@ -38,6 +38,12 @@ int inputConnectedMask(void); * mouse-aim accumulator maintained by inputUpdate(). */ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y); +/* Grab/release the mouse (relative-mouse mode). The host event pump calls + * this on window focus loss/gain so alt-tabbing frees the cursor. A release + * also suspends mouse-aim reads until re-grabbed. No-op if the mouse is + * disabled in config. */ +void inputSetMouseGrab(int on); + #ifdef __cplusplus } #endif diff --git a/port/src/input.c b/port/src/input.c index fc31c918..89e89de2 100644 --- a/port/src/input.c +++ b/port/src/input.c @@ -123,6 +123,7 @@ static int connectedMask = 0x1; /* controller 0 always present */ static SDL_GameController *pads[MAX_PADS]; static int mouseEnabled = 1; +static int mouseGrabbed = 1; /* released while the window is unfocused */ static int mouseAimSpeed = 50; /* aim-mode sensitivity, percent */ static int mouseTurnSpeed = 100; /* hipfire yaw sensitivity, percent */ static int mouseInvertY = 0; /* 1 = mouse-down looks up */ @@ -182,6 +183,7 @@ int inputInit(void) inputOpenPads(); /* Relative mouse mode for mouse-look. ESC still quits (video.c). */ + mouseGrabbed = mouseEnabled; if (mouseEnabled) { SDL_SetRelativeMouseMode(SDL_TRUE); /* Drain the initial jump. */ @@ -212,7 +214,7 @@ void inputUpdate(void) { SDL_GameControllerUpdate(); - if (!mouseEnabled) { + if (!mouseEnabled || !mouseGrabbed) { return; } @@ -379,6 +381,20 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y) return button; } +void inputSetMouseGrab(int on) +{ + int want = on && mouseEnabled; + if (want == mouseGrabbed) { + return; + } + mouseGrabbed = want; + SDL_SetRelativeMouseMode(want ? SDL_TRUE : SDL_FALSE); + if (want) { + SDL_GetRelativeMouseState(NULL, NULL); /* drain the accumulated jump */ + } + mouseDX = mouseDY = 0.0; +} + int inputConnectedMask(void) { return connectedMask; diff --git a/port/src/video.c b/port/src/video.c index 532bc6fd..ae37b679 100644 --- a/port/src/video.c +++ b/port/src/video.c @@ -26,6 +26,7 @@ #include "platform.h" #include "system.h" #include "video.h" +#include "input.h" #include "../fast3d/gfx_api.h" #include "../fast3d/gfx_sdl.h" @@ -46,6 +47,9 @@ static struct GfxRenderingAPI *renderingAPI; static int initDone = 0; static u32 frames = 0; +/* Set by the host event pump (F12), consumed on the render thread in + * videoEndFrame where a GL context is current. */ +static volatile int screenshotReq = 0; static double fpsWindowStart = 0.0; static int fpsNumFrames = 0; static float vidAvgFPS = 0.f; @@ -147,6 +151,8 @@ void videoPumpEvents(void) if (ev.key.keysym.sym == SDLK_ESCAPE) { sysLogPrintf(LOG_INFO, "video: ESC -> quit"); exit(0); + } else if (ev.key.keysym.sym == SDLK_F12 && !ev.key.repeat) { + screenshotReq = 1; } break; case SDL_WINDOWEVENT: @@ -155,6 +161,10 @@ void videoPumpEvents(void) exit(0); } else if (ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { gfx_sdl_update_cached_size(); + } else if (ev.window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + inputSetMouseGrab(0); /* free the cursor when alt-tabbed away */ + } else if (ev.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) { + inputSetMouseGrab(1); } break; default: @@ -201,6 +211,21 @@ void videoEndFrame(void) } } + if (screenshotReq) { + screenshotReq = 0; + static int shotNum = 0; + char path[128]; + GE_MKDIR("ppm"); + snprintf(path, sizeof(path), "ppm/shot_%03d.ppm", shotNum++); + if (gfx_opengl_dump_bound_fbo((uint32_t)gfx_current_dimensions.width, + (uint32_t)gfx_current_dimensions.height, path)) { + sysLogPrintf(LOG_INFO, "video: screenshot -> %s " + "(view with tools_pc/ppm2bmp.py)", path); + } else { + sysLogPrintf(LOG_WARNING, "video: screenshot failed"); + } + } + ++frames; ++fpsNumFrames;