port: absolute menu cursor + dual-pump event fix + auto window size (M-35)

Follow-on to ad748654 (D180). Playtested this session: ESC-to-free now
works; Dam -> Surface seamless on Agent.

- Dual SDL event pump race: port/src/video.c (host thread) and
  port/fast3d/gfx_sdl2.cpp (render thread) both drain the same SDL queue,
  so either loop can dequeue a given event. ESC-to-free / click-to-lock /
  mouse wheel / focus events were being eaten by the render-thread pump
  and lost. Now handled in BOTH pumps (inputReleaseCapture /
  inputNotifyClick / inputPostWheel / inputSetMouseGrab).

- Menu pointer rewritten for capture mode. front.c's cursor integrator
  caps at ~5.75 virtual px/poll, so the D165/D169 P-controller fed from
  an absolute mouse position was always laggy/floaty and could not reach
  the bottom of the grid. With MouseCaptureMode=1 the front end has a
  real free OS cursor: write cursor_h_pos/v_pos directly from the
  absolute mouse position and emit a zero stick. Legacy relative path
  (MouseCaptureMode=0) unchanged, and its estimate clamp now tracks the
  live front-end rect (was pinned to 320x240 -> right/bottom unreachable).

- OS cursor hidden while the window is focused (applyCursorVisibility);
  shown again on focus loss.

- Window.Width/Height <= 0 now auto-fits a 4:3 window to ~85% of the
  desktop (gfx_sdl2 gfx_sdl_init) instead of 640x480 native. video.c
  passes 0 through; [Window] persistence still pins whatever you leave.

Verified: build-pc.sh ntsc-final green 240/240; -level_09 framediff
3/3 within threshold (nonclear d <= 0.01pp, phash = documented D117).

Claude-Session: https://claude.ai/code/session_01GULf6knbsqfsnW39tMr9WT
This commit is contained in:
James Dansereau committed 2026-09-02 09:54:17 -04:00
1 parent 10b278311f
commit 433907b566
3 files changed
+133 -51

No files matched your search

+34
View File
@@ -7,6 +7,7 @@
#include "gfx_window_manager_api.h"
#include "gfx_screen_config.h"
#include "input.h"
static SDL_Window* wnd;
static SDL_GLContext ctx;
@@ -100,6 +101,22 @@ static void gfx_sdl_init(const struct GfxWindowInitSettings *set) {
sysFatalError("Could not init SDL:\n%s", SDL_GetError());
}
// Auto window size: a width/height <= 0 means "pick a sensible default from
// the desktop" rather than the 640x480 native res. Fit a 4:3 window into
// ~85% of the primary desktop -- big enough to actually judge mouse/menu
// behaviour, and 4:3 so the pointer maps onto the render with no letterbox.
if (window_width <= 0 || window_height <= 0) {
SDL_DisplayMode dm;
if (SDL_GetDesktopDisplayMode(0, &dm) == 0 && dm.w > 0 && dm.h > 0) {
int maxw = (int)(dm.w * 0.85);
int maxh = (int)(dm.h * 0.85);
if (maxw * 3 > maxh * 4) { window_height = maxh; window_width = maxh * 4 / 3; }
else { window_width = maxw; window_height = maxw * 3 / 4; }
}
if (window_width < 640 || window_height < 480) { window_width = 640; window_height = 480; }
sysLogPrintf(LOG_INFO, "SDL: auto window size %dx%d", window_width, window_height);
}
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
@@ -303,8 +320,21 @@ static void gfx_sdl_handle_events(void) {
// D145: Alt+F4 quits; bare ESC no longer does (it is the
// menu "back" key -- see port/src/video.c / input.c).
exit(0);
} else if (event.key.keysym.sym == SDLK_ESCAPE && !event.key.repeat) {
// WI-1: this render-thread pump and port/src/video.c's
// host-thread pump both drain the same SDL queue, so either
// one can be the loop that dequeues a given ESC keydown.
// Handle it here too or click-to-lock's ESC-to-free races
// (and usually loses to this loop, which runs every frame).
inputReleaseCapture();
}
break;
case SDL_MOUSEBUTTONDOWN:
inputNotifyClick(); // WI-1: (re)lock in click-to-lock mode
break;
case SDL_MOUSEWHEEL:
inputPostWheel(event.wheel.y); // weapon cycle
break;
case SDL_WINDOWEVENT:
if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
SDL_GL_GetDrawableSize(wnd, &window_width, &window_height);
@@ -316,6 +346,10 @@ static void gfx_sdl_handle_events(void) {
// We listen specifically for main window close because closing main window
// on macOS does not trigger SDL_Quit.
exit(0);
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST) {
inputSetMouseGrab(0); // free + show cursor on alt-tab (also handled by video.c's pump; whichever dequeues it)
} else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED) {
inputSetMouseGrab(1);
}
break;
case SDL_QUIT:
+92 -45
View File
@@ -163,6 +163,7 @@ extern float cursor_h_pos, cursor_v_pos;
static void applyGrab(int want);
static void reconcileGrab(int menuMode);
static void applyCursorVisibility(void);
static int numControllers = 1;
static int connectedMask = 0x1; /* controller 0 always present */
@@ -180,7 +181,7 @@ static int mouseGrabbed = 1; /* released while the window is unfocused */
* Re-entering a stage while still "armed" re-locks automatically so
* unpausing / starting a level does not need a click.
* Controller input is entirely independent of all of this. */
static int mouseCaptureMode = 0;
static int mouseCaptureMode = 1; /* WI-1 default: Quake-style click-to-lock + 1:1 menu pointer. 0 = legacy always-grab. */
static int captureArmed = 0; /* user has clicked to lock (capture mode) */
static int windowFocused = 1;
static int mouseAimSpeed = 16; /* aim-mode sensitivity, percent (B3: 50 -> 25 M-29 -> 16; still overshot at 25) */
@@ -221,6 +222,7 @@ static double menuEstH = 0.0, menuEstV = 0.0; /* estimate of the game cursor (
static double menuTgtH = 0.0, menuTgtV = 0.0; /* mouse-driven target (virtual px) */
static int menuPrevActive = 0;
static double hipPitchPhase = 0.0; /* D166: hipfire pitch pulse phase 0..1 */
static int lastMenuMouseX = -1, lastMenuMouseY = -1; /* WI-2: last abs cursor seen in a menu */
/* Integrate one poll of our cursor estimate with front.c's exact recurrence
* (frontUpdateControlStickPosition): the game receives `stick` as an s8, applies
@@ -427,6 +429,8 @@ int inputInit(void)
SDL_GetRelativeMouseState(NULL, NULL);
}
applyCursorVisibility(); /* hide the OS cursor if we start focused */
sysLogPrintf(LOG_INFO, "input: ready (mask=0x%x, %d controller(s), aimSpeed=%d)",
connectedMask, numControllers, mouseAimSpeed);
return connectedMask;
@@ -576,13 +580,29 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
sx += (int)(edx * g);
sy -= (int)(dyLook * g); /* front-end cursor: +sy = up */
} else if (menuMode) {
/* Front-end 1:1 pointer (D165): P-controller onto a
* mouse-accumulated target. */
/* D169: clamp bounds from the live virtual screen
* (front.c frontUpdateControlStickPosition clamps the real
* cursor to [screenleft+20, screenleft+screenwidth-20] x
* [screentop+20, screentop+screenheight-20]); fall back to the
* old 320x240 constants if the front-end screen isn't set. */
/* Front-end menu pointer.
*
* front.c frontUpdateControlStickPosition() is the game's cursor
* integrator: it reads joyGetStickX/Y, applies a +/-5 deadband,
* clamps the stick to +/-70, then does
* cursor_h_pos += (stick*0.075 +/- 0.5) * timerDelta
* and clamps cursor_h/v_pos into [screenleft+20 ..
* screenleft+screenwidth-20] x [screentop+20 ..
* screentop+screenheight-20]. That integrator caps the cursor at
* ~5.75 virtual px per poll, so feeding it a stick derived from
* an absolute mouse position (D165/D169 P-controller) is
* unavoidably laggy/floaty and desyncs from the real cursor.
*
* With click-to-lock (MouseCaptureMode=1) the front end has a
* real free OS cursor, so we know exactly where the pointer is.
* Write cursor_h/v_pos DIRECTLY from the absolute mouse position
* whenever the mouse moved, and emit a zero stick so the game's
* integrator adds nothing (stick 0 -> deadband). When the mouse
* is idle we leave the cursor alone and let the keyboard stick
* (WASD/arrows, added above) drive it through the game as usual.
* cursor_h/v_pos are the plain front.c UI globals every menu
* hit-test already reads -- no logic change, just where the
* pointer is placed. */
double loH = MENU_CURSOR_LO, hiH = MENU_CURSOR_HI_H;
double loV = MENU_CURSOR_LO, hiV = MENU_CURSOR_HI_V;
{
@@ -593,18 +613,7 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
loV = st + 20.0; hiV = st + sh - 20.0;
}
}
if (!menuPrevActive) {
/* Seed the estimate from the real menu cursor, not a
* 320x240 centre guess. */
menuEstH = menuTgtH = cursor_h_pos;
menuEstV = menuTgtV = cursor_v_pos;
hipPitchPhase = 0.0;
}
/* WI-2: with click-to-lock the front end has a real free OS
* cursor. Map its absolute window position straight onto the
* virtual front-end rect so the game crosshair tracks it 1:1
* (no drift -- the P-controller below just chases this target).
* The relative-delta path (legacy / grabbed) is unchanged. */
int haveAbs = 0;
if (mouseCaptureMode && !mouseGrabbed) {
int mx = 0, my = 0;
@@ -613,40 +622,65 @@ unsigned inputComputePad(int idx, signed char *stick_x, signed char *stick_y)
int ww = 0, wh = 0;
if (w) SDL_GetWindowSize(w, &ww, &wh);
if (ww > 0 && wh > 0) {
double fx = (double)mx / (double)ww;
double fy = (double)my / (double)wh;
if (fx < 0.0) fx = 0.0; else if (fx > 1.0) fx = 1.0;
if (fy < 0.0) fy = 0.0; else if (fy > 1.0) fy = 1.0;
menuTgtH = loH + fx * (hiH - loH);
menuTgtV = loV + fy * (hiV - loV);
haveAbs = 1;
if (!menuPrevActive) {
/* just entered a menu: adopt the current pointer as
* the baseline, don't yank the cursor this frame */
lastMenuMouseX = mx;
lastMenuMouseY = my;
}
if (mx != lastMenuMouseX || my != lastMenuMouseY) {
double fx = (double)mx / (double)ww;
double fy = (double)my / (double)wh;
if (fx < 0.0) fx = 0.0; else if (fx > 1.0) fx = 1.0;
if (fy < 0.0) fy = 0.0; else if (fy > 1.0) fy = 1.0;
cursor_h_pos = (float)(loH + fx * (hiH - loH));
cursor_v_pos = (float)(loV + fy * (hiV - loV));
sx = 0; /* pointer owns the cursor this poll */
sy = 0;
}
lastMenuMouseX = mx;
lastMenuMouseY = my;
}
}
double s = (menuPointerSpeed / 100.0) * MENU_MOUSE_TO_PX;
if (!haveAbs) {
/* Legacy relative path (MouseCaptureMode=0, cursor grabbed):
* P-controller onto a mouse-accumulated target (D165/D169).
* D180: clamp the estimate to the same live rect as the
* target -- D169 widened the target clamp but left the
* estimate pinned to the old 320x240 constants, so the
* right/bottom of the menu was unreachable and the
* controller wound up into never-settling drift. */
if (!menuPrevActive) {
menuEstH = menuTgtH = cursor_h_pos;
menuEstV = menuTgtV = cursor_v_pos;
hipPitchPhase = 0.0;
}
double s = (menuPointerSpeed / 100.0) * MENU_MOUSE_TO_PX;
menuTgtH += edx * s;
menuTgtV += dyLook * s; /* dyLook > 0 => cursor moves down (+V) */
menuTgtV += dyLook * s;
if (menuTgtH < loH) menuTgtH = loH;
if (menuTgtH > hiH) menuTgtH = hiH;
if (menuTgtV < loV) menuTgtV = loV;
if (menuTgtV > hiV) menuTgtV = hiV;
double effH = MENU_P_GAIN * (menuTgtH - menuEstH);
double effV = MENU_P_GAIN * (menuTgtV - menuEstV);
if (effH > 70.0) effH = 70.0; else if (effH < -70.0) effH = -70.0;
if (effV > 70.0) effV = 70.0; else if (effV < -70.0) effV = -70.0;
menuEstH = menuCursorStep(menuEstH, effH, loH, hiH);
menuEstV = menuCursorStep(menuEstV, effV, loV, hiV);
sx += (int)(effH + (effH >= 0.0 ? 0.5 : -0.5));
sy -= (int)(effV + (effV >= 0.0 ? 0.5 : -0.5));
}
if (menuTgtH < loH) menuTgtH = loH;
if (menuTgtH > hiH) menuTgtH = hiH;
if (menuTgtV < loV) menuTgtV = loV;
if (menuTgtV > hiV) menuTgtV = hiV;
double effH = MENU_P_GAIN * (menuTgtH - menuEstH);
double effV = MENU_P_GAIN * (menuTgtV - menuEstV);
if (effH > 70.0) effH = 70.0; else if (effH < -70.0) effH = -70.0;
if (effV > 70.0) effV = 70.0; else if (effV < -70.0) effV = -70.0;
menuEstH = menuCursorStep(menuEstH, effH, MENU_CURSOR_LO, MENU_CURSOR_HI_H);
menuEstV = menuCursorStep(menuEstV, effV, MENU_CURSOR_LO, MENU_CURSOR_HI_V);
sx += (int)(effH + (effH >= 0.0 ? 0.5 : -0.5));
sy -= (int)(effV + (effV >= 0.0 ? 0.5 : -0.5)); /* game sticky = -sy; sticky>0 drives cursor +V */
if (configGetInputLog()) {
sysLogPrintf(LOG_NOTE,
"GE_INPUTLOG menuptr est=(%.1f,%.1f) tgt=(%.1f,%.1f) eff=(%.1f,%.1f) stick=(%d,%d)",
menuEstH, menuEstV, menuTgtH, menuTgtV, effH, effV, sx, sy);
"GE_INPUTLOG menuptr abs=%d cursor=(%.1f,%.1f) stick=(%d,%d)",
haveAbs, (double)cursor_h_pos, (double)cursor_v_pos, sx, sy);
}
} else if (aimHeld) {
double gx = fabs(edx) * (mouseAimSpeed / 100.0) * AIM_GAIN;
@@ -787,12 +821,25 @@ static void reconcileGrab(int menuMode)
applyGrab(want);
}
/* Hide the OS cursor while the game window is focused (normal PC-game
* behaviour): in a stage the mouse drives the look axis, and in menus GE draws
* its own crosshair that now tracks the pointer 1:1 -- a visible OS arrow on
* top is just clutter. Show it again when focus is lost so the desktop behaves
* normally. Relative-mouse mode hides the cursor too, but only while grabbed;
* this covers the free-but-focused states (menus, pre-click stage). */
static void applyCursorVisibility(void)
{
int hide = windowFocused && mouseEnabled;
SDL_ShowCursor(hide ? SDL_DISABLE : SDL_ENABLE);
}
/* video.c focus events. In legacy mode this directly grabs/releases; in
* capture mode it just records focus and lets reconcileGrab() decide. */
void inputSetMouseGrab(int on)
{
windowFocused = on ? 1 : 0;
reconcileGrab(0); /* menuMode re-checked on the next poll anyway */
applyCursorVisibility();
}
/* A mouse click landed in the game window (video.c). In click-to-lock mode
+7 -6
View File
@@ -60,10 +60,11 @@ static int cfgWrapFix = 0; /* D74 sub-tile UV pre-wrap + RC3/D167 non-Po
static int cfgFullscreen = 0; /* 0 = windowed, 1 = borderless fullscreen */
/*
* [Window] persistence. Defaults are sentinels that reproduce the old
* hardcoded behaviour (native size, centered): W/H = 0 -> native, X/Y = -1 ->
* let SDL centre the window. videoSaveWindowState() writes the live geometry
* back into these on a clean exit (see main.c's atexit handler).
* [Window] persistence. W/H = 0 -> auto (gfx_sdl2 fits a 4:3 window into ~85%
* of the desktop); X/Y = -1 -> let SDL centre the window.
* videoSaveWindowState() writes the live geometry back into these on a clean
* exit (see main.c's atexit handler), so after the first run the file pins
* whatever size you left it at.
*/
static int cfgWinW = 0;
static int cfgWinH = 0;
@@ -117,8 +118,8 @@ int videoInit(void)
/* MSAA: snap the requested sample count down to a supported power of two. */
gfx_msaa_level = cfgMSAA >= 8 ? 8 : cfgMSAA >= 4 ? 4 : cfgMSAA >= 2 ? 2 : 1;
int winW = cfgWinW > 0 ? cfgWinW : GE_NATIVE_W;
int winH = cfgWinH > 0 ? cfgWinH : GE_NATIVE_H;
int winW = cfgWinW > 0 ? cfgWinW : 0; /* 0 -> gfx_sdl2 auto-fits to the desktop */
int winH = cfgWinH > 0 ? cfgWinH : 0;
int havePos = (cfgWinX >= 0 && cfgWinY >= 0);
struct GfxInitSettings set = {