SDL Platform Layer
How the SDL window, event pump, keyboard, and mouse bridge into an engine that still expects Win32-style input and deferred resize behavior.
The SDL port is not a clean-room platform rewrite. It is a compatibility layer that creates the window and gathers input through SDL, then feeds the result back into the existing Win32-oriented engine contracts. That is why the SDL files in Platform/ spend so much effort translating into old engine message shapes instead of introducing a new input abstraction.
Window creation starts in initializeAppWindowsSDL inside GeneralsMD/Code/Main/WinMain.cpp. The function chooses the requested resolution from globals, decides whether the window should start maximized, and calls Platform::SDLPlatform::Init. One subtle detail there is important: when -xres or -yres was passed explicitly, startup disables maximized mode so SDL does not silently replace the requested resolution with the desktop work area. After the window is created, the code pushes the actual client size back into TheGlobalData before the renderer is created so the swap chain matches the real SDL window.
Platform::SDLPlatform in Platform/SDLPlatform.cpp owns the low-level window. Init creates a resizable SDL window, chooses bordered windowed mode or borderless fullscreen-style mode, explicitly maximizes when requested, starts SDL text input, and caches the final width and height. ToggleBorderless is the runtime counterpart. It restores the window out of maximized state, flips the border flag, then either sizes to the display work area for bordered-maximized mode or to the full display bounds for borderless mode. It also pushes the new size into the registered resize callback immediately so the renderer is not one present behind the real window size.
Event delivery is split between the platform object and the game engine wrapper. SDLPlatform::PumpEvents polls SDL events, lets the Inspector overlay consume them first, forwards unclaimed input to a registered callback, and still handles platform-level events such as quit, resize, focus gain/loss, minimize, and restore. SDLGameEngine::init registers the event callback before parent engine initialization, then retrieves the SDL-backed mouse and keyboard instances that the game client created.
The callback itself, SDLEventDispatcher in SDLGameEngine.cpp, is where most of the compatibility work happens. Mouse motion, button, and wheel events are forwarded directly to SDLMouse. Key events go to SDLKeyboard, except for one hard-coded engine behavior:
const bool isEnter = (event.key.scancode == SDL_SCANCODE_RETURN || event.key.scancode == SDL_SCANCODE_KP_ENTER); const bool altHeld = (SDL_GetModState() & SDL_KMOD_ALT) != 0 || (event.key.mod & SDL_KMOD_ALT) != 0; if (isEnter && altHeld) { if (!event.key.repeat) Platform::SDLPlatform::Instance().ToggleBorderless(); return; // swallow so Enter doesn't leak into menus/chat }
Note the double-check on modifier state — SDL_GetModState() OR event.key.mod. The event's mod field was observed to be stale on the down-event for some keyboard layouts, so the live query acts as a fallback. Both down and up events match the same swallow condition so the key state doesn't desync.
Text entry is even more Win32-shaped. SDL gives the engine SDL_EVENT_TEXT_INPUT as UTF-8 text, but existing widgets expect already-composed characters through GWM_IME_CHAR. The dispatcher decodes UTF-8 codepoints and feeds them one at a time into the old IME/window-manager path. It also synthesizes control-code GWM_IME_CHAR events for Enter on key-down because SDL text input does not produce WM_CHAR-style control characters on its own:
// SDL3's SDL_EVENT_TEXT_INPUT only fires for printable characters. // Win32 WM_CHAR delivered Enter as 0x0D, which GadgetTextEntry expects // to submit the line. Synthesize it on key-down. if (!event.key.repeat && TheIMEManager && TheWindowManager) { GameWindow* target = TheIMEManager->getWindow(); if (target) { unsigned int cp = 0; switch (event.key.scancode) { case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: cp = 0x0D; break; // VK_RETURN default: break; } if (cp != 0) TheWindowManager->winSendInputMsg(target, GWM_IME_CHAR, (WindowMsgData)cp, 0); } }
The TheIMEManager->getWindow() gate ensures the synthetic Enter only fires when a text-input widget is active — menu Enter (which uses the scancode/GWM_CHAR path) is not double-dispatched. See sdl3_text_input.md for the plumbing history.
SDLMouse and SDLKeyboard finish the translation. SDLMouse::init forces absolute-motion semantics because SDL delivers window-relative positions, not Win32-style deltas. It preloads .ANI cursors through TheFileSystem, keeps the OS cursor visible even when the game requests cursor hiding, only captures the cursor in fullscreen-like mode, and multiplies wheel input by 120 so the existing zoom code sees Win32 WHEEL_DELTA values. SDLKeyboard maps SDL scancodes to DirectInput-style DIK values and writes them into the same ring-buffer shape that the legacy keyboard code already consumes.
The last bridge point is W3DGameClient::createKeyboard and createMouse in GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DGameClient.cpp. When USE_SDL is enabled, those factories return SDLKeyboard and SDLMouse instead of Win32 devices, and the old TheWin32Mouse global is deliberately nulled because SDL is now the source of truth for pointer events.
Why Deferred Resize Still Exists
SDLGameEngine does not apply a full resize directly inside the SDL event callback. It only sets the global gPendingResize, matching the old Win32 deferred-resize path. The comment explains why: snap and maximize gestures can emit a burst of resize events, and rebuilding swap-chain state synchronously inside the poll loop caused crashes. The inherited Win32 update path already knows how to apply the latest size safely between frames, so the SDL layer reuses it.
Quirks
- The SDL port still routes printable text through the engine's IME message path instead of inventing a new widget-input protocol.
- Alt+Enter is handled in the SDL dispatcher, not in gameplay key bindings, so the keystroke never leaks into chat or menu controls.
- Cursor capture only happens in fullscreen-like mode. Windowed mode intentionally leaves the pointer free for title bars and other monitors.
- Mouse-wheel values are scaled to Win32's
120-unit convention so existing zoom code keeps working unchanged.