How SDL2 Powers Window Management and Input Handling in Equilibrium Engine
SDL2 initializes once at startup to create native windows, poll events, and translate input into ECS components, enabling cross-platform windowing and input handling in the Equilibrium Engine.
The Equilibrium Engine (clibequilibrium/equilibriumengine) uses SDL2 as its low-level platform abstraction layer. By wrapping SDL2 calls inside a dedicated SdlSystem, the engine exposes windowing and input capabilities through flecs ECS components that other systems can read and modify.
SDL2 Initialization and System Setup
The engine initializes SDL2 exactly once during the import phase of the SdlSystem.
One-Time Initialization Pattern
In equilibrium/systems/sdl_system.c, the SdlSystemImport function boots SDL and registers a shutdown callback:
void SdlSystemImport(world_t *world) {
if (SDL_Init(SDL_INIT_EVERYTHING) != 0) {
ecs_err("Unable to initialize SDL: %s", SDL_GetError());
return;
}
ecs_atfini(world, SdlShutdown, NULL);
}
This pattern ensures SDL_Init(SDL_INIT_EVERYTHING) runs before any window creation, while ecs_atfini guarantees SDL_Quit() executes automatically when the ECS world shuts down, preventing resource leaks.
Creating and Managing Windows
Window lifecycle management is handled through ECS observers that react to component changes rather than direct API calls.
The AppWindow Component
When an entity receives an AppWindow component, the observer SdlCreateWindow triggers automatically in equilibrium/systems/sdl_system.c. It constructs the SDL window flags, creates the native window, and updates the component with actual drawable dimensions:
static void SdlCreateWindow(ecs_iter_t *it) {
AppWindow *app_window = ecs_field(it, AppWindow, 1);
SDL_Window *created_window = SDL_CreateWindow(
title, x, y, app_window[i].width, app_window[i].height, flags);
SDL_GL_GetDrawableSize(created_window, &actual_width, &actual_height);
app_window_handle->value = created_window;
app_window[i].width = actual_width;
app_window[i].height = actual_height;
}
The code stores the SDL_Window* pointer in a companion AppWindowHandle component and overwrites the requested width/height with the actual drawable size returned by SDL_GL_GetDrawableSize, ensuring correct pixel ratios on high-DPI displays.
Maximizing and Display Bounds
For maximized windows, the system queries usable display bounds and forces borderless fullscreen mode by calculating screen dimensions and setting appropriate SDL window flags before creation (lines 15-33 in sdl_system.c).
Input Handling and Event Translation
Input processing occurs every frame via the OnInput tag, translating SDL events into the engine's unified Input component format.
The Event Loop
The SdlProcessEvents system runs continuously, draining the SDL event queue each frame:
static void SdlProcessEvents(ecs_iter_t *it) {
Input *input = ecs_field(it, Input, 1);
while (SDL_PollEvent(&e)) {
if (input->callback) input->callback(&e);
if (e.type == SDL_KEYDOWN) {
uint32_t sym = key_sym(e.key.keysym.sym,
input->keys['S'].state != 0);
key_down(&input->keys[sym]);
} else if (e.type == SDL_KEYUP) {
uint32_t sym = key_sym(e.key.keysym.sym,
input->keys['S'].state != 0);
key_up(&input->keys[sym]);
}
else if (e.type == SDL_MOUSEBUTTONDOWN) { ... }
else if (e.type == SDL_MOUSEMOTION) { ... }
else if (e.type == SDL_MOUSEWHEEL) { ... }
else if (e.type == SDL_WINDOWEVENT) { ... }
}
}
The loop forwards raw SDL events to an optional user callback, then maps each event to the engine's internal representation. This design keeps SDL-specific code isolated while providing game systems with platform-agnostic input data.
Keyboard Input Mapping
The key_sym() function (lines 10-45 in sdl_system.c) translates SDL scancodes to engine-specific ECS_KEY_* constants defined in equilibrium/components/input.h. It handles printable ASCII ranges, shifted symbols, and special keys including arrow keys, Ctrl, Shift, and Alt, returning a uint32_t index into the input->keys[] array.
Mouse State Tracking
Mouse events update three distinct fields in the MouseState structure:
- Button presses:
input->mouse.leftandinput->mouse.righttrack pressed/held states - Position:
input->mouse.wndstores window-relative coordinates, whileinput->mouse.relstores relative movement delta - Scrolling:
input->mouse.scrollaccumulates wheel values fromSDL_MOUSEWHEELevents
Window Lifecycle and Cleanup
Automatic Destruction
When an entity loses its AppWindowHandle component—either through deletion or component removal—the observer SdlDestroyWindow executes:
static void SdlDestroyWindow(ecs_iter_t *it) {
AppWindowHandle *window = ecs_field(it, AppWindowHandle, 1);
for (int i = 0; i < it->count; i++) {
SDL_DestroyWindow(window[i].value);
}
}
This ECS-driven destruction pattern ensures SDL_DestroyWindow always pairs with previously created windows, preventing dangling window pointers or memory leaks.
ECS Integration
Component Registration
The SDL integration exposes two primary component types in equilibrium/components/sdl_window.h and equilibrium/components/input.h:
// sdl_window.h
typedef struct SdlWindow { int32_t dummy; } SdlWindow;
EQUILIBRIUM_API extern ECS_COMPONENT_DECLARE(SdlWindow);
// Registration in sdl_window.c
void SdlComponentsImport(world_t *world) {
ECS_MODULE(world, SdlComponents);
ECS_COMPONENT_DEFINE(world, SdlWindow);
}
The SdlWindow component acts as a tag that triggers the window creation observer, while the Input component (defined in input.h with KeyState and MouseState structures) stores processed input data accessible to any system querying input.components.Input.
Summary
- Single initialization:
SdlSystemImportcallsSDL_Init(SDL_INIT_EVERYTHING)once and registersSDL_Quit()viaecs_atfini - Component-driven windows: Adding
AppWindowtriggers automatic SDL window creation; removingAppWindowHandletriggers destruction - Event translation:
SdlProcessEventspollsSDL_PollEventeach frame and maps SDL keys toECS_KEY_*constants viakey_sym() - Unified input: Mouse and keyboard states are written to the
Inputcomponent, allowing systems to query input without including SDL headers - Resource safety: Native SDL resources are managed through ECS observers, ensuring cleanup occurs automatically when entities are destroyed
Frequently Asked Questions
How does the engine ensure SDL2 resources are cleaned up properly?
The engine registers SdlShutdown as a world finalizer using ecs_atfini in SdlSystemImport. This guarantees SDL_Quit() executes automatically when the ECS world shuts down, regardless of how the application exits. Additionally, the SdlDestroyWindow observer calls SDL_DestroyWindow immediately when an AppWindowHandle component is removed from an entity.
Can I access raw SDL events instead of the processed Input component?
Yes. The Input component includes an optional callback function pointer that SdlProcessEvents invokes for every SDL_PollEvent iteration before performing its own translation. Set this callback during system initialization to receive raw SDL_Event data for custom handling while still benefiting from the engine's automatic window management.
What keyboard keys are supported by the key_sym translation function?
The key_sym() function in sdl_system.c handles the full printable ASCII range (space through tilde), arrow keys, function keys, and modifier keys (Ctrl, Shift, Alt). The complete list of supported constants is defined in equilibrium/components/input.h (lines 38-117), including ECS_KEY_SPACE, ECS_KEY_ESCAPE, and directional arrows.
How does the engine handle high-DPI displays with SDL2?
During window creation in SdlCreateWindow, the code calls SDL_GL_GetDrawableSize immediately after SDL_CreateWindow. This retrieves the actual pixel dimensions, which may differ from the requested logical size on high-DPI monitors. The system then updates the AppWindow component's width and height fields with these actual drawable dimensions, ensuring rendering operates at the correct resolution.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →