Skip to content

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

Open
githubawn wants to merge 21 commits into
TheSuperHackers:mainfrom
githubawn:feature/sdl3-input-backport
Open

feat(input): Implement SDL3 input and window management#2639
githubawn wants to merge 21 commits into
TheSuperHackers:mainfrom
githubawn:feature/sdl3-input-backport

Conversation

@githubawn
Copy link
Copy Markdown

@githubawn githubawn commented Apr 20, 2026

SDL3 Input Backend
This PR implements an SDL3-based input and windowing backend as a modern alternative 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
Animated cursor support via .ani (RIFF) file loading.

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) Precision Mode
RT (hold) Force Attack (LCTRL)
LB Select All (Q)
RB Queue Orders (LSHIFT)
A Left click
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.

The foundation of this backend was built using the SDL3 input work from the generalsX fork by fbraz3.

Todo: replicate to generals

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

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

This PR introduces a complete SDL3-based input and windowing backend as a compile-time alternative to the legacy Win32/DirectInput path, selectable via the SAGE_USE_SDL3 CMake option (enabled by default for modern builds). The implementation centralises keyboard, mouse, and gamepad events through a unified ring-buffer manager and adds animated .ani/RIFF cursor loading via SDL3_image.

  • SDL3InputManager / SDL3Mouse / SDL3Keyboard — new input layer with coordinate scaling, directional cursor animation, and a hardcoded gamepad mapping that emits synthetic mouse and keyboard events.
  • SDL3GameEngine — new GameEngine subclass handling window creation, text-input routing (with custom UTF-8 decoder), minimised-window throttling, and all factory methods wired to the existing W3D/Miles subsystems.
  • WinMain.cpp — SDL3 window and splash-screen path added under SAGE_USE_SDL3 guards; vcpkg/FetchContent dependency management added via cmake/sdl3.cmake.

Confidence Score: 5/5

Safe to merge; all new code is behind the SAGE_USE_SDL3 compile flag and does not touch the legacy Win32/DirectInput path

The SDL3 backend is entirely additive and compile-flag gated. The ring-buffer event routing, coordinate scaling, and gamepad mapping are structurally sound. The issues found are style nits (NULL vs nullptr, single-line if body) and a type-punning pattern in the RIFF parser that is unlikely to produce incorrect results in practice but should be cleaned up before targeting Linux/Wine builds.

SDL3Cursor.cpp — strict-aliasing type-punning in RIFF tag comparisons; WinMain.cpp — minor style violations in the SDL3 init block

Important Files Changed

Filename Overview
Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Input.cpp New SDL3 mouse, keyboard, and input manager implementation; ring-buffer event routing, gamepad mapping, and coordinate scaling are all correctly structured
Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp New animated-cursor loader with RIFF/ACON parsing; three type-punning comparisons using *(Uint32*)"tag" are UB under strict aliasing — should use memcmp
Core/GameEngineDevice/Source/SDL3GameEngine.cpp New SDL3 game engine with window management, text input routing, UTF-8 decoding, and factory methods; logic is sound and headless path correctly falls through to GameEngine::init()
GeneralsMD/Code/Main/WinMain.cpp SDL3 window/splash screen init added under SAGE_USE_SDL3 guard; two NULL usages should be nullptr and one single-line if body violates project style
Core/GameEngineDevice/Include/SDL3Device/GameClient/SDL3Input.h New header declaring SDL3Mouse, SDL3Keyboard, and SDL3InputManager; uses #pragma once and nullptr throughout; gamepad constants defined as constexpr
GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DGameClient.h Factory methods for keyboard/mouse now branch on SAGE_USE_SDL3 to return SDL3 or legacy Win32 devices; Win32Mouse extern correctly guarded
cmake/sdl3.cmake vcpkg-first SDL3 discovery with FetchContent fallback; static build configured with required Windows system libs linked to SDL3-static

Sequence Diagram

sequenceDiagram
    participant WM as WinMain
    participant SGE as SDL3GameEngine
    participant SIM as SDL3InputManager
    participant SM as SDL3Mouse
    participant SK as SDL3Keyboard
    participant GE as GameEngine

    WM->>WM: SDL_Init + SDL_CreateWindow
    WM->>WM: Set TheSDL3Window
    WM->>SGE: CreateGameEngine()
    SGE->>SGE: init()
    SGE->>SIM: new SDL3InputManager(window)
    SIM->>SIM: openFirstGamepad()
    SGE->>GE: GameEngine::init()
    GE->>SM: createMouse() → SDL3Mouse(TheSDL3Window)
    GE->>SK: createKeyboard() → SDL3Keyboard()

    loop Per Frame
        SGE->>SGE: update()
        SGE->>SGE: pollSDL3Events()
        SGE->>SIM: update()
        SIM->>SIM: SDL_PollEvent loop
        SIM->>SIM: addMouseSDLEvent / addKeyboardSDLEvent
        SIM->>SIM: processGamepadInput()
        SIM-->>SIM: virtualPulseMouse / virtualPulseKey
        SGE->>GE: GameEngine::update()
        GE->>SM: getMouseEvent()
        SM->>SIM: getNextMouseEvent()
        SM->>SM: translateEvent() + scaleMouseCoordinates()
        GE->>SK: getKey()
        SK->>SIM: getNextKeyboardEvent()
    end
Loading
Prompt To Fix All With AI
Fix the following 5 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 5
GeneralsMD/Code/Main/WinMain.cpp:924
`NULL` is used as a null pointer literal in two places within the new SDL3 block. The codebase rule requires `nullptr` for all null pointer literals in C++ code.

```suggestion
			ApplicationHWnd = (HWND)SDL_GetPointerProperty(props, SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr);
```

### Issue 2 of 5
GeneralsMD/Code/Main/WinMain.cpp:913
The `if` body sits on the same line as the condition. Per project style, the body should always be on a separate line to allow precise debugger breakpoint placement.

```suggestion
			if (!TheGlobalData->m_windowed)
				flags |= SDL_WINDOW_FULLSCREEN;
```

### Issue 3 of 5
Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp:123-127
Casting a string-literal pointer to `Uint32*` and dereferencing it (`*(Uint32*)"anih"`) is undefined behaviour in C++ due to strict-aliasing rules. GCC/Clang with `-O2` can exploit this to produce wrong results when cross-compiled on Linux. Using `memcmp` (as already done for the outer RIFF/ACON check) is safe and equally readable. The same pattern appears for `"LIST"` and `"icon"` elsewhere in this function.

```suggestion
			if (memcmp(&id, "anih", 4) == 0 && sz >= 36)
			{
				memcpy(&rate, p + 28, 4);
			}
			else if (memcmp(&id, "LIST", 4) == 0 && sz >= 4 && memcmp(p, "fram", 4) == 0)
```

### Issue 4 of 5
Core/GameEngineDevice/Source/SDL3Device/GameClient/SDL3Cursor.cpp:138
Same strict-aliasing UB as the sibling comparisons above — replace the type-punned dereference with `memcmp`.

```suggestion
					if (memcmp(&fid, "icon", 4) == 0)
```

### Issue 5 of 5
GeneralsMD/Code/Main/WinMain.cpp:940
The `NULL` passed as the source-rect argument to `SDL_BlitSurfaceScaled` should be `nullptr` — it is a pointer and the project rules require `nullptr` for all null pointer literals in C++ code.

Reviews (22): Last reviewed commit: "Merge remote-tracking branch 'upstream/m..." | Re-trigger 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
OVERRIDE_FIND_PACKAGE
)

FetchContent_Declare(
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Do you think we need SDL3_image? Looks like it's used to parse .ico for animated cursors? Maybe there is already code for that we can adapt

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

directinput lets windows directly handle the cursor rendering so new code and SDL3_image was needed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In that case consider putting AnimatedCursor in its own file.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

SDL3_image seem overkill for handling animated cursors though as @stephanmeesters says, its just been used to decode the ico format, SDL itself already provides support for the animated cursors once the files are decoded and loaded.

Copy link
Copy Markdown
Author

@githubawn githubawn Apr 21, 2026

Choose a reason for hiding this comment

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

Moved AnimatedCursor into its own file and switched to the SDL_CreateAnimatedCursor API.

Regarding the dependency: this is a conscious choice. While SDL core handles the animation logic, it doesn't decode ANI or ICO files. I’d rather offload that to SDL_image than maintain bespoke binary parsing. Using the library also makes it trivial for modders to use modern formats in the future.

I think this is the most maintainable path. Does that sound reasonable?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Regarding the dependency: this is a conscious choice. While SDL core handles the animation logic, it doesn't decode ANI or ICO files. I’d rather offload that to SDL_image than maintain bespoke binary parsing. Using the library also makes it trivial for modders to use modern formats in the future.

I think this is the most maintainable path. Does that sound reasonable?

Also @bobtista ’s #1785 could then use it for jpg and png encoding

Comment thread GeneralsMD/Code/Main/WinMain.cpp
Comment thread cmake/config-build.cmake
# Enable SDL3 by default for modern builds
if(NOT IS_VS6_BUILD)
option(SAGE_USE_SDL3 "Enable SDL3 input/window backend" ON)
if(SAGE_USE_SDL3)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Maybe call it USE_SDL3

Comment thread cmake/config-build.cmake
@@ -9,6 +9,16 @@ option(RTS_BUILD_OPTION_ASAN "Build code with Address Sanitizer." OFF)
option(RTS_BUILD_OPTION_VC6_FULL_DEBUG "Build VC6 with full debug info." OFF)
option(RTS_BUILD_OPTION_FFMPEG "Enable FFmpeg support" OFF)

# Enable SDL3 by default for modern builds
if(NOT IS_VS6_BUILD)
option(SAGE_USE_SDL3 "Enable SDL3 input/window backend" ON)
Copy link
Copy Markdown

@stephanmeesters stephanmeesters Apr 20, 2026

Choose a reason for hiding this comment

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

Options should be on top of the file and be something like RTS_BUILD_OPTION_SDL3


namespace {

Bool DecodeNextUtf8Codepoint(const char* text, size_t length, size_t& offset, UnsignedInt& outCodepoint)
Copy link
Copy Markdown

@stephanmeesters stephanmeesters Apr 20, 2026

Choose a reason for hiding this comment

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

Do we need this, maybe better placed in UnicodeString? Or some other helper if it's generic stuff

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point.

I looked at #2045 and #2528 which are working on UTF-8 infrastructure.
If one of those lands first, forwardTextInputEvent could use the bulk Utf8_To_Utf16Le conversion and iterate the resulting wchar_t string directly, making this function unnecessary.

Happy to refactor once there's a clear winner between those two. For now it's self-contained here.

@githubawn githubawn force-pushed the feature/sdl3-input-backport branch from e365605 to 627ef23 Compare April 20, 2026 18:24
@githubawn githubawn changed the title feat(input): Implement SDL3 input backend feat(input): Implement SDL3 input and window management Apr 20, 2026
Consolidated all work from the test/sdl3-backport branch into a single atomic commit:
- Centralized input management via SDL3InputManager.
- Hardened Ani/RIFF cursor loading with robust bounds checking.
- Native Gamepad support with analogue stick-to-mouse emulation and custom RTS mappings.
- Modernized focus and capture handling for better stability during Alt-Tab.
- Standardized and secured string operations throughout the SDL3 path.
Consolidated all work from the test/sdl3-backport branch into a single atomic commit:
- Centralized input management via SDL3InputManager.
- Hardened Ani/RIFF cursor loading with robust bounds checking.
- Native Gamepad support with analogue stick-to-mouse emulation and custom RTS mappings.
- Modernized focus and capture handling for better stability during Alt-Tab.
- Standardized and secured string operations throughout the SDL3 path.
- Updated credit attribution (fbraz3).
@githubawn githubawn 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
}
}

AnimatedCursor* SDL3CursorManager::loadANI(const char* filepath)
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

libsdl-org/SDL_image#730
Ran into a bug in SDL_image. Ignore SDL3CursorManager::loadANI if you are reviewing this.

@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
Copy link
Copy Markdown

@xezon xezon left a comment

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
"git-tree": "3f05e04b9aededb96786a911a16193cdb711f0c9"
}
]
"version": 1,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Indent changed in the json files.

Comment thread CMakeLists.txt
include(cmake/gamespy.cmake)
include(cmake/lzhl.cmake)

if(SAGE_USE_SDL3 AND NOT IS_VS6_BUILD)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Where does the SAGE_ prefix come from? We have not used that before for anything else.


#if SAGE_USE_SDL3
#include <SDL3/SDL.h>
#include "SDL3GameEngine.h"
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 SDL code is added to a file called WinMain (for Windows). Is this intentional?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This was intentional. The SDL3 input backend is designed with a future splitscreen skirmish feature in mind, supporting multiple simultaneous mice/keyboards so that 2–8 players can share a single machine rather than needing separate computers, either controlling the same or unique armies.

SDL3 input doesn't function without an SDL window, so creating the window here was the minimum viable approach to make the input backend functional. The Win32/DirectInput path doesn't lend itself to the multi-device model SDL3 enables, which is why SDL3 is the foundation for this direction.


ParticleSystemManager* SDL3GameEngine::createParticleSystemManager(Bool dummy)
{
(void)dummy;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why not create dummy here?


namespace {

Bool DecodeNextUtf8Codepoint(const char* text, size_t length, size_t& offset, UnsignedInt& outCodepoint)
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 function looks out of place. Bobtista was also working on Utf8 decoding in another change. It would be good to have this as a utility somewhere accessible engine wide, and not specific to this file.

m_gamepad(nullptr),
m_precisionMode(FALSE),
m_lastUpdateTime(0),
m_isQuitting(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.

Or use false

*/
Bool SDL3InputManager::getNextMouseEvent(SDL_Event& outEvent)
{
if (m_mouseEvents[m_mouseNextGet].type == SDL_EVENT_FIRST) return 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.

New line. Many times in this review.

handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_RIGHT, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_RIGHT], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT), [&](bool d){ virtualPulseKey(SDL_SCANCODE_3, d); });
handleGamepadButton(SDL_GAMEPAD_BUTTON_DPAD_DOWN, m_state.buttonState[SDL_GAMEPAD_BUTTON_DPAD_DOWN], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN), [&](bool d){ virtualPulseKey(SDL_SCANCODE_4, d); });
handleGamepadButton(SDL_GAMEPAD_BUTTON_LEFT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_LEFT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_LEFT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_SELECT_NEXT_IDLE_WORKER); });
handleGamepadButton(SDL_GAMEPAD_BUTTON_RIGHT_STICK, m_state.buttonState[SDL_GAMEPAD_BUTTON_RIGHT_STICK], SDL_GetGamepadButton(m_gamepad, SDL_GAMEPAD_BUTTON_RIGHT_STICK), [&](bool d){ if (d) TheMessageStream->appendMessage(GameMessage::MSG_META_VIEW_COMMAND_CENTER); });
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

These lines are awfully long.

Comment thread cmake/config-build.cmake

# Enable SDL3 by default for modern builds
if(NOT IS_VS6_BUILD)
option(SAGE_USE_SDL3 "Enable SDL3 input/window backend" ON)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Please do not use SAGE_ keywords. We use RTS_

Maybe do RTS_SDL3_ENABLE. That is consistent style with RTS_CRASHDUMP_ENABLE

Comment thread cmake/config-build.cmake

# Enable SDL3 by default for modern builds
if(NOT IS_VS6_BUILD)
option(SAGE_USE_SDL3 "Enable SDL3 input/window backend" ON)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why is this default enabled on non VS6 builds but Windows builds? Will SDL3 perform better than Win API?

Copy link
Copy Markdown
Author

@githubawn githubawn May 14, 2026

Choose a reason for hiding this comment

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

If we don't use DirectStorage and Raytracing the SDL3 boilerplate performs better than our current implementation even on Windows in trade for a slightly larger binary. Legacy version doesn't compile statistically against SDL3 and so VS6 changes are avoided.

@xezon
Copy link
Copy Markdown

xezon commented May 12, 2026

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

@githubawn
Copy link
Copy Markdown
Author

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

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.

5 participants