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.left and input->mouse.right track pressed/held states
  • Position: input->mouse.wnd stores window-relative coordinates, while input->mouse.rel stores relative movement delta
  • Scrolling: input->mouse.scroll accumulates wheel values from SDL_MOUSEWHEEL events

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: SdlSystemImport calls SDL_Init(SDL_INIT_EVERYTHING) once and registers SDL_Quit() via ecs_atfini
  • Component-driven windows: Adding AppWindow triggers automatic SDL window creation; removing AppWindowHandle triggers destruction
  • Event translation: SdlProcessEvents polls SDL_PollEvent each frame and maps SDL keys to ECS_KEY_* constants via key_sym()
  • Unified input: Mouse and keyboard states are written to the Input component, 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →