Skip to content

feat(input): Implement SDL3 input and window management - #2639

Draft
mirelle7 wants to merge 56 commits into
TheSuperHackers:mainfrom
mirelle7:feature/sdl3-input-backport
Draft

mirelle7 wants to merge 56 commits into
TheSuperHackers:mainfrom
mirelle7:feature/sdl3-input-backport

Conversation

@mirelle7

@mirelle7 mirelle7 commented Apr 20, 2026 •

Copy link
Copy Markdown

SDL3 Input Backend
This PR updates the windowing and input pipeline to use SDL3 by default on modern toolchains (non-VC6 builds) to DirectInput and Win32 window creation. By utilizing SDL3, we bypass DirectInput emulation layers on modern systems, providing a lower-latency pipeline for Windows 11 and Wine/Linux users.

Input handling
Unified event manager that centralizes keyboard, mouse, and gamepad events into a thread-safe buffer

Gamepad Support (v0.1)
This is an initial baseline implementation focused on providing functional out-of-the-box playability. Feedback is encouraged regarding the ergonomics and logic of these default mappings.

Input Action
Left Stick Virtual mouse cursor
Right Stick Camera pan (arrow keys)
LT (hold) Currently unmapped
RT (hold) Force Attack (LCTRL)
LB Select All (Q)
RB Queue Orders (LSHIFT)
A Left click
AA Guard mode
B Right click
X Attack Move (A)
Y Stop (S)
D-Pad Control groups 1–4
Start Menu (ESC)
Back Last radar event (SPACE)
L3 Next idle worker
R3 Snap to command center

Scope Note: This implementation provides a hardcoded default layout to establish core functionality. Advanced features, such as input remapping, radial menus, adjustable deadzones, are currently out of scope for this PR and may be addressed in future iterations.

This SDL3 input/windowing backend builds on prior work from the community:

@mirelle7
mirelle7 force-pushed the feature/sdl3-input-backport branch from a785545 to 7cc0861 Compare April 20, 2026 15:00
@greptile-apps

greptile-apps Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The SDL3 backend reaches the runtime window and input paths for both games on modern builds.

Fix closing the minimized game and satisfy the required statement format before merging.

Findings

  1. P1 Minimized game will not close ▶
  2. P2 Statement bodies share breakpoint lines ▶

Summary

Adds SDL3 window creation and input for modern builds, while retaining the legacy backend as an option.

  • Modern builds start the games through an SDL3 window.
  • SDL3 input turns keyboard, mouse, and gamepad events into game actions.
  • Controller movement changes how the camera scrolls.
  • Builds can select SDL3 and bring in its required libraries.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[SDL events] --> B[SDL3InputManager]
    B --> C[Keyboard and mouse queues]
    C --> D[Game update]
    D --> E[Message handlers]
    B --> F[Quit message]
    F --> E
    G[Minimized loop] --> B
    B --> G
    E --> H[Quit game]
Loading

Reviews (39) · Last reviewed commit: "fix(sdl3): keep virtual double-click tim..." · Reviewed by Greptile

Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp
Comment thread cmake/sdl3.cmake Outdated
Comment thread cmake/sdl3.cmake
Comment thread GeneralsMD/Code/Main/WinMain.cpp Outdated
Comment thread cmake/config-build.cmake Outdated
Comment thread cmake/config-build.cmake Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp Outdated
@mirelle7
mirelle7 force-pushed the feature/sdl3-input-backport branch from e365605 to 627ef23 Compare April 20, 2026 18:24
@mirelle7 mirelle7 changed the title feat(input): Implement SDL3 input backend feat(input): Implement SDL3 input and window management Apr 20, 2026
@mirelle7
mirelle7 force-pushed the feature/sdl3-input-backport branch from eb9908a to 534e694 Compare April 21, 2026 01:17
Comment thread GeneralsMD/Code/Main/WinMain.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3GameEngine.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp
@DevGeniusCode DevGeniusCode added this to the Linux support milestone Apr 29, 2026
@xezon xezon added Gen Relates to Generals ZH Relates to Zero Hour Platform Work towards platform support, such as Linux, MacOS Input labels May 12, 2026

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs polishing. Not yet reviewed extensively until the obvious style issues are fixed.

Comment thread vcpkg-lock.json Outdated
Comment thread CMakeLists.txt Outdated
Comment thread GeneralsMD/Code/Main/WinMain.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
Comment thread cmake/config-build.cmake Outdated
Comment thread cmake/config-build.cmake Outdated
@xezon

xezon commented May 12, 2026

Copy link
Copy Markdown

Perhaps also check Fighter19's fork for SDL related implementations. As far as I am aware he has it all done.

@mirelle7

Copy link
Copy Markdown
Author

I have looked at all existing forks and attributed where possible.

Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp
@bobtista

bobtista commented Jun 3, 2026 •

Copy link
Copy Markdown

How about changing display-mode? eg getDisplayModeCount() / getDisplayModeDescription() using SDL_GetFullscreenDisplayModes + desktop mode + resolution filtering. EDIT: Also account for mouse position which can sometimes not match clicks to cursor position.

@bobtista

bobtista commented Jun 3, 2026

Copy link
Copy Markdown

Is it better to use SDL3InputManager (+ SDL3CursorManager) or to split out SDL3Keyboard + SDL3Mouse that subclass the engine's existing Keyboard/Mouse base classes?

Comment thread vcpkg-lock.json Outdated

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we have a SDL3Main or not?

Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp Outdated
@mirelle7

Copy link
Copy Markdown
Author

I've started cleaning up the style and structural issues in the latest commits.

Regarding SDL3Main: The main benefit of the current approach is that we keep the game's native Win32 WinMain entry point and startup sequence intact.

To get rid of the #ifdef blocks in WinMain while minimizing duplication, I can move the window initialization out into SDL3Window.

Regarding splitting out subclasses: I don't think splitting the input handling into isolated SDL3Keyboard and SDL3Mouse subclasses is the right way. It runs contrary to how SDL3 tries to be a unified way; it relies on a single event pump where all keyboard, mouse, and gamepad signals flow together.

@xezon

xezon commented Jun 24, 2026

Copy link
Copy Markdown

I don't think splitting the input handling into isolated SDL3Keyboard and SDL3Mouse subclasses is the right way.

But this is how you currently have set this up. What is the plan here?

mirelle7 and others added 18 commits October 19, 2026 23:04
Co-authored-by: Bobby Battista <bobtista@gmail.com>
SDL3 loads .ani cursors via TheFileSystem rather than Win32's
LoadCursorFromFile, enabling cursor loading from .BIG archives
and virtual filesystems. Add a non-positive size check before
allocating memory buffers to handle empty or invalid files safely.
- Use event.wheel.mouse_x/y directly instead of calling SDL_GetMouseState,
  ensuring accurate cursor position at the exact moment of the wheel event.
- Replace Win32 timeGetTime() with SDL keyEvent.timestamp in SDL3Keyboard::getKey
  for portable millisecond key-down timing.
- Replace Win32 VK_RETURN macro with character code 13 in IME Return key dispatch.
combo box text dropping fixed
Fix the crash when launching a second instance.
Timestamp injected gamepad events so key repeat works.
Ignore gamepad input while the window is unfocused.
Let the window actually resize to the chosen resolution.
…oints

SDL3Main.cpp had no SetUnhandledExceptionFilter/try-catch, losing crash
dumps on that path; move WinMain.cpp's handler into AppMain so both
entry points install it once instead of duplicating it, and wrap
AppMain::run() in a catch-all so both paths get equivalent coverage.
…heck

The shared fallback branch called FetchContent_MakeAvailable(SDL3)
whenever SDL3_image's version check failed, even if SDL3 was already
found via find_package - defining its targets twice and stopping the
configure. Give each library its own guarded fallback branch.
Restore ALT-key release (resetKeys instead of the no-op reset), audio
mute/unmute, and stuck gamepad-driven virtual keys on window focus
change, and stop double-counting gamepad cursor motion deltas.
loadANI now returns nullptr instead of a cursor with a null SDL_Cursor
when creation fails; clamp viewport-scaled mouse coordinates to the
last valid pixel instead of one past it; skip loading the splash BMP
in headless mode; drop the dead addSDLEvent wrappers and unused
SDL3CursorManager::init(); use snprintf for the splash path buffer.
setScrolling/stopScrolling were retrofitted with null checks for
TheMouse/TheDisplay et al. but canScrollAtScreenEdge still
dereferenced TheMouse and TheDisplay unconditionally.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · 🎯 Functional Correctness · SDL3Input.cpp:948

Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp:948
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The review comment is supported. virtualPulseMouse() keeps lastClickTime across all SDL3InputManager instances and does not reset it when virtual gamepad inputs are released. A reconnect or manager recreation within 350 ms can mark the first virtual mouse press as a double click. Move the timestamps into SDL3InputManager and reset them in releaseGamepadVirtualInputs().


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: db189db8-6cfb-452c-b2a2-49b47c374e0b
📥 Commits

Reviewing files that changed from the base of the PR and between 138be1c and f9e8aea.

📒 Files selected for processing: 2
  • Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h
  • Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp
🚧 Files skipped from review as they are similar to previous changes: 1
  • Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 2 remain after this review.

Motion can fill the mouse ring between frames, dropping the button-ups
releaseGamepadVirtualInputs() queues on focus loss or gamepad disconnect
and leaving a virtual mouse button or key held. The last
RESERVED_RELEASE_EVENTS slots of both rings now only accept
button-up/key-up events.
@mirelle7
mirelle7 force-pushed the feature/sdl3-input-backport branch from f9e8aea to 72252bc Compare October 19, 2026 21:08
…actors

- BIGFileSystem replaces StdBIGFileSystem
- MiniDumper moved to its own library and takes a product id
- TheDebugLogCriticalSection is gone
- drop BaseType.h include and WIN32_LEAN_AND_MEAN
@mirelle7
mirelle7 force-pushed the feature/sdl3-input-backport branch from 72252bc to 6ba77d1 Compare October 19, 2026 21:09
@mirelle7
mirelle7 marked this pull request as ready for review October 19, 2026 21:10
… them on release

The timestamps were a function-local static, shared by every
SDL3InputManager and never cleared, so the first virtual press after a
focus loss or gamepad reconnect could count as a double-click.
else
snprintf(resourcePath, sizeof(resourcePath), "Data/Cursors/%s.ani", mouse->m_cursorInfo[cursor].textureName.str());

m_cursorResources[cursor][direction] = loadANI(resourcePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium GameClient/SDL3Cursor.cpp:74

initResources ignores loose cursor overrides in TheGlobalData->m_modDir, so directory mods display the base game's cursor or fail to load a cursor that exists only in the mod. It always passes the base Data/Cursors/... path to FileSystem::openFile, which does not prepend the mod directory; try the mod-prefixed path first, then fall back to the base path.

🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp around line 74:

`initResources` ignores loose cursor overrides in `TheGlobalData->m_modDir`, so directory mods display the base game's cursor or fail to load a cursor that exists only in the mod. It always passes the base `Data/Cursors/...` path to `FileSystem::openFile`, which does not prepend the mod directory; try the mod-prefixed path first, then fall back to the base path.

Comment on lines +1058 to +1066
float ly = SDL_GetGamepadAxis(m_gamepad, SDL_GAMEPAD_AXIS_LEFTY) / AXIS_MAX;
float stickMagnitude = sqrtf(lx * lx + ly * ly);
if (stickMagnitude > 1.0f)
stickMagnitude = 1.0f;

if (stickMagnitude > DEADZONE)
{
// Instant polar direction matching exact stick angle (0 rotational lag, 0 loops)
float dirX = lx / stickMagnitude;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium GameClient/SDL3Input.cpp:1058

Diagonal left-stick input produces cursor velocity above targetSpeed: after stickMagnitude is clamped to 1, (0.9, 0.9) is divided by 1, yielding a direction with length about 1.27. Preserve the original magnitude for direction normalization and clamp only the magnitude used for the response.

 	float stickMagnitude = sqrtf(lx * lx + ly * ly);
+	float directionMagnitude = stickMagnitude;
 	if (stickMagnitude > 1.0f)
 		stickMagnitude = 1.0f;
@@
-		float dirX = lx / stickMagnitude;
-		float dirY = ly / stickMagnitude;
+		float dirX = lx / directionMagnitude;
+		float dirY = ly / directionMagnitude;
🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp around lines 1058-1066:

Diagonal left-stick input produces cursor velocity above `targetSpeed`: after `stickMagnitude` is clamped to 1, `(0.9, 0.9)` is divided by 1, yielding a direction with length about 1.27. Preserve the original magnitude for direction normalization and clamp only the magnitude used for the response.

Comment thread Core/Main/AppMain.cpp
Comment on lines +85 to +92
// Set working directory to executable folder
Char buffer[_MAX_PATH];
GetModuleFileName(nullptr, buffer, sizeof(buffer));
if (Char* pEnd = strrchr(buffer, '\\'))
{
*pEnd = 0;
}
::SetCurrentDirectory(buffer);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High Main/AppMain.cpp:85

Launches using -useCwd or -setCwd keep the executable directory as the working directory, so relative asset paths resolve from the wrong location and files may fail to load. This unconditional SetCurrentDirectory(buffer) overrides an earlier parseCommandLineForStartup() call, while the later call at line 101 skips parsing once it has already run; remove this duplicate directory change and let the existing CommandLine/WorkingDirectory logic choose the directory.

- 	// Set working directory to executable folder
- 	Char buffer[_MAX_PATH];
- 	GetModuleFileName(nullptr, buffer, sizeof(buffer));
- 	if (Char* pEnd = strrchr(buffer, '\\'))
-	{
-		*pEnd = 0;
-	}
-	::SetCurrentDirectory(buffer);
🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/Main/AppMain.cpp around lines 85-92:

Launches using `-useCwd` or `-setCwd` keep the executable directory as the working directory, so relative asset paths resolve from the wrong location and files may fail to load. This unconditional `SetCurrentDirectory(buffer)` overrides an earlier `parseCommandLineForStartup()` call, while the later call at line 101 skips parsing once it has already run; remove this duplicate directory change and let the existing `CommandLine`/`WorkingDirectory` logic choose the directory.


for (size_t i = 0; i < wideLength; ++i)
{
TheWindowManager->winSendInputMsg(targetWindow, GWM_IME_CHAR, static_cast<WindowMsgData>(wideText[i]), 0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium Common/SDL3GameEngine.cpp:249

Typing committed text into a focused editable combo drops every character, so its text cannot be updated. GadgetComboBoxInput ignores GWM_IME_CHAR on the combo itself; resolve combo targets to GadgetComboBoxGetEditBox() before dispatching the characters.

Suggested change
TheWindowManager->winSendInputMsg(targetWindow, GWM_IME_CHAR, static_cast<WindowMsgData>(wideText[i]), 0);
TheWindowManager->winSendInputMsg(BitIsSet(targetWindow->winGetStyle(), GWS_COMBO_BOX) ? GadgetComboBoxGetEditBox(targetWindow) : targetWindow, GWM_IME_CHAR, static_cast<WindowMsgData>(wideText[i]), 0);
🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/GameEngineDevice/Source/SDL3Device/Common/SDL3GameEngine.cpp around line 249:

Typing committed text into a focused editable combo drops every character, so its text cannot be updated. `GadgetComboBoxInput` ignores `GWM_IME_CHAR` on the combo itself; resolve combo targets to `GadgetComboBoxGetEditBox()` before dispatching the characters.

Comment thread Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp
return;
}

SDL_Event nextEvent;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium GameClient/SDL3Input.cpp:416

An unmapped scancode makes getKey() return KEY_NONE while later keyboard events remain queued, so Keyboard::updateKeys() stops processing and delays releases such as an arrow-key release. Skip unmapped events in getKey() and return KEY_NONE only when the queue is empty.

🤖 Copy this AI Prompt to have your agent fix this:
In file @Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp around line 416:

An unmapped scancode makes `getKey()` return `KEY_NONE` while later keyboard events remain queued, so `Keyboard::updateKeys()` stops processing and delays releases such as an arrow-key release. Skip unmapped events in `getKey()` and return `KEY_NONE` only when the queue is empty.

Comment on lines +714 to +716
if (TheMessageStream && TheMessageStream->isReadyForMessages())
{
TheMessageStream->appendMessage(GameMessage::MSG_META_DEMO_INSTANT_QUIT);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Minimized game will not close

Closing the game while it is minimized can leave it running indefinitely. When TheMessageStream is ready, this handler only queues MSG_META_DEMO_INSTANT_QUIT. The minimized loop keeps polling events but never calls GameEngine::update() to consume that message, so getQuitting() stays false.

Set a quit flag that this loop can see, or leave the loop when a close request arrives.

Comment on lines +887 to +890
if (m_state.stickLeft) virtualPulseKey(SDL_SCANCODE_LEFT, false);
if (m_state.stickRight) virtualPulseKey(SDL_SCANCODE_RIGHT, false);
if (m_state.stickUp) virtualPulseKey(SDL_SCANCODE_UP, false);
if (m_state.stickDown) virtualPulseKey(SDL_SCANCODE_DOWN, false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Statement bodies share breakpoint lines

The new release handlers put each if body on the same line as its condition. The repository requires if, else, for, and while bodies on separate lines so developers can place breakpoints on the body. Split these statements across lines before merging.

The same pattern appears in the button callbacks at lines 1189 and 1243, and elsewhere in this file.

Rule Used: Always place if/else/for/while statement bodies on separate lines from the condition to allow precise debugger breakpoint placement. Prefer the format: cpp if (condition) doSomethingNextLine(); over: ```cpp if (condition) doSomethingOnSam... (source)

Learned From
TheSuperHackers/GeneralsGameCode#2067

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@greptile-apps

greptile-apps Bot commented Oct 19, 2026

Copy link
Copy Markdown

Comments Outside Diff

These findings could not be posted inline.

  • P1 m pos.z left uninitialized Core/GameEngine/Source/GameClient/View.cpp:63 ▶

    m_pos.zero() was replaced with explicit x/y assignments, but m_pos.z is never set. This propagates garbage into groundToCameraVector.z in GameAudio.cpp via groundToCameraVector.sub(&groundPos), which runs before groundPos.z is assigned from TheTerrainLogic. The wrong zScale = desiredHeight / groundToCameraVector.z value can misplace the audio microphone.

    The same omission occurs in View::init() around line 91.

    	m_pos.x = 0;
    	m_pos.y = 0;
    	m_pos.z = 0;
    

@mirelle7
mirelle7 marked this pull request as draft October 19, 2026 21:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gen Relates to Generals Input Platform Work towards platform support, such as Linux, MacOS ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants