# How SDL2 Powers Window Management and Input Handling in Equilibrium Engine

> Learn how SDL2 powers window management and input handling in the Equilibrium Engine. Discover cross-platform event polling and input translation into ECS components.

- Repository: [Alexander/equilibriumengine](https://github.com/clibequilibrium/equilibriumengine)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/systems/sdl_system.c), the `SdlSystemImport` function boots SDL and registers a shutdown callback:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/systems/sdl_system.c). It constructs the SDL window flags, creates the native window, and updates the component with actual drawable dimensions:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/sdl_system.c)) translates SDL scancodes to engine-specific `ECS_KEY_*` constants defined in [`equilibrium/components/input.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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:

```c
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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/components/sdl_window.h) and [`equilibrium/components/input.h`](https://github.com/clibequilibrium/equilibriumengine/blob/main/equilibrium/components/input.h):

```c
// 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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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`](https://github.com/clibequilibrium/equilibriumengine/blob/main/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.