M-24: playtest QoL — F12 screenshot + mouse frees on focus loss

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%).
This commit is contained in:
James Dansereau committed 2026-08-30 12:08:22 -04:00
1 parent 546d803870
commit 46fa579e54
5 files changed
+64 -6

No files matched your search

+8 -1
View File
@@ -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)
+8 -4
View File
@@ -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
+6
View File
@@ -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
+17 -1
View File
@@ -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;
+25
View File
@@ -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;